From 68b82e7e0c0ae4477f382cf51cc571ba2a577139 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 10 Jun 2026 13:12:04 +0200 Subject: [PATCH 1/3] Reapply "Merge pull request #197 from microsoft/sandy081/multi-chat-impl" This reverts commit 58a3fead0f19397a7e52c7ee7afaef150876d981. --- CHANGELOG.md | 15 + clients/go/CHANGELOG.md | 17 +- clients/go/ahp/multi_host_state_mirror.go | 28 +- clients/go/ahp/reducers.go | 444 +- clients/go/ahp/reducers_fixture_test.go | 11 +- clients/go/ahptypes/actions.generated.go | 594 +- clients/go/ahptypes/commands.generated.go | 32 +- clients/go/ahptypes/common.go | 22 + .../go/ahptypes/notifications.generated.go | 5 +- clients/go/ahptypes/state.generated.go | 557 +- clients/go/examples/reducers_demo/main.go | 36 +- clients/kotlin/CHANGELOG.md | 16 +- .../microsoft/agenthostprotocol/Reducers.kt | 684 +-- .../generated/Actions.generated.kt | 435 +- .../generated/Commands.generated.kt | 48 + .../generated/Notifications.generated.kt | 5 +- .../generated/State.generated.kt | 443 +- .../DiscriminatedUnionTest.kt | 32 +- .../FixtureDrivenReducerTest.kt | 18 +- .../agenthostprotocol/ReducersTest.kt | 116 +- clients/rust/CHANGELOG.md | 17 +- clients/rust/crates/ahp-types/src/actions.rs | 414 +- clients/rust/crates/ahp-types/src/commands.rs | 44 +- .../crates/ahp-types/src/notifications.rs | 5 +- clients/rust/crates/ahp-types/src/state.rs | 314 +- .../crates/ahp/src/multi_host_state_mirror.rs | 23 +- clients/rust/crates/ahp/src/reducers.rs | 513 +- .../ahp/tests/multi_host_state_mirror.rs | 7 +- .../Generated/Actions.generated.swift | 496 +- .../Generated/Commands.generated.swift | 57 + .../Generated/Notifications.generated.swift | 5 +- .../Generated/State.generated.swift | 431 +- .../AgentHostProtocol/NativeReducer.swift | 696 +-- .../Sources/AgentHostProtocol/Reducers.swift | 504 +- .../AHPStateMirror.swift | 19 +- .../MultiHostStateMirror.swift | 18 +- .../AHPClientTests.swift | 2 +- .../AHPStateMirrorTests.swift | 4 +- .../MultiHostStateMirrorTests.swift | 6 +- .../FixtureDrivenReducerTests.swift | 4 + .../NativeReducerTests.swift | 38 +- .../ReducersTests.swift | 65 +- clients/swift/CHANGELOG.md | 16 +- clients/typescript/CHANGELOG.md | 16 +- docs/.vitepress/config.mts | 2 + docs/specification/chat-channel.md | 137 + docs/specification/session-channel.md | 84 +- schema/actions.schema.json | 5373 +++++++++-------- schema/commands.schema.json | 4665 +++++++------- schema/errors.schema.json | 3629 +++++------ schema/notifications.schema.json | 3576 +++++------ schema/state.schema.json | 3838 ++++++------ scripts/find-protocol-sources.ts | 1 + scripts/generate-action-origin.ts | 35 +- scripts/generate-go.ts | 272 +- scripts/generate-kotlin.ts | 231 +- scripts/generate-markdown.ts | 31 + scripts/generate-rust.ts | 229 +- scripts/generate-swift.ts | 231 +- types/action-origin.generated.ts | 206 +- types/actions.ts | 1 + types/channels-chat/actions.ts | 544 ++ types/channels-chat/commands.ts | 60 + types/channels-chat/reducer.ts | 663 ++ types/channels-chat/state.ts | 1149 ++++ types/channels-session/actions.ts | 506 +- types/channels-session/commands.ts | 18 +- types/channels-session/reducer.ts | 647 +- types/channels-session/state.ts | 1087 +--- types/commands.ts | 1 + types/common/actions.ts | 147 +- types/common/messages.ts | 6 + types/common/state.ts | 4 +- types/index.ts | 106 +- types/messages.test.ts | 1 + types/reducers.test.ts | 22 +- types/reducers.ts | 1 + types/state.ts | 1 + .../reducers/003-session-ready.json | 4 +- .../reducers/004-session-creationfailed.json | 6 +- .../reducers/005-session-turnstarted.json | 34 +- ...messageid-removes-from-queuedmessages.json | 54 +- ...messageid-removes-last-queued-message.json | 36 +- ...eid-removes-matching-steering-message.json | 36 +- ...ageid-does-not-touch-pending-messages.json | 54 +- .../010-session-delta-appends-content.json | 38 +- ...sion-delta-with-wrong-turnid-is-no-op.json | 34 +- ...ion-delta-without-activeturn-is-no-op.json | 34 +- ...on-responsepart-adds-to-responseparts.json | 34 +- ...4-session-turncomplete-finalizes-turn.json | 38 +- ...-session-turncancelled-finalizes-turn.json | 34 +- ...ssion-error-finalizes-turn-with-error.json | 34 +- ...-force-cancels-in-progress-tool-calls.json | 36 +- ...rncomplete-with-wrong-turnid-is-no-op.json | 34 +- ...-start-delta-ready-confirmed-complete.json | 42 +- ...h-auto-confirm-transitions-to-running.json | 36 +- ...-call-denied-transitions-to-cancelled.json | 38 +- ...-result-confirmation-pending-approved.json | 40 +- ...d-cancelled-with-result-denied-reason.json | 40 +- ...nding-confirmation-defaults-confirmed.json | 38 +- ...ions-for-unknown-toolcallid-are-no-op.json | 34 +- ...ing-tool-back-to-pending-confirmation.json | 38 +- ...-approved-transitions-back-to-running.json | 40 +- ...ation-denied-transitions-to-cancelled.json | 40 +- ...-non-streaming-non-running-tool-calls.json | 34 +- ...30-session-titlechanged-updates-title.json | 4 +- ...on-usage-updates-usage-on-active-turn.json | 34 +- ...n-reasoning-appends-reasoning-content.json | 38 +- ...33-session-modelchanged-updates-model.json | 4 +- ...-servertoolschanged-sets-server-tools.json | 6 +- ...ssion-activeclientchanged-sets-client.json | 6 +- ...ion-activeclientchanged-unsets-client.json | 8 +- ...ctiveclienttoolschanged-updates-tools.json | 8 +- ...changed-without-activeclient-is-no-op.json | 4 +- .../reducers/039-set-steering-message.json | 34 +- ...040-replace-existing-steering-message.json | 34 +- ...-message-content-via-set-with-same-id.json | 34 +- .../reducers/042-remove-steering-message.json | 34 +- ...ng-message-is-no-op-for-mismatched-id.json | 34 +- ...ing-message-is-no-op-when-none-exists.json | 34 +- .../045-set-a-new-queued-message.json | 34 +- ...-append-queued-message-when-id-is-new.json | 34 +- ...ssage-in-place-when-id-already-exists.json | 34 +- .../reducers/048-remove-a-queued-message.json | 34 +- ...ueued-message-sets-array-to-undefined.json | 34 +- ...ueued-message-is-no-op-for-unknown-id.json | 34 +- ...-message-is-no-op-when-array-is-empty.json | 34 +- ...g-and-queued-messages-are-independent.json | 36 +- .../053-reorders-queued-messages.json | 34 +- ...not-in-order-at-end-in-original-order.json | 34 +- .../055-ignores-unknown-ids-in-reorder.json | 34 +- ...serves-all-messages-in-original-order.json | 34 +- ...s-no-op-when-no-queued-messages-exist.json | 34 +- ...mizationschanged-replaces-entire-list.json | 6 +- ...nged-replaces-existing-customizations.json | 8 +- ...on-customizationtoggled-toggles-by-id.json | 8 +- ...zationtoggled-is-no-op-for-unknown-id.json | 8 +- ...s-no-op-when-customizations-undefined.json | 4 +- ...on-truncated-keeps-turns-up-to-turnid.json | 34 +- ...uncated-keeps-all-when-turnid-is-last.json | 34 +- ...keeps-only-first-when-turnid-is-first.json | 34 +- ...ncated-clears-all-when-turnid-omitted.json | 34 +- ...7-session-truncated-drops-active-turn.json | 34 +- ...ps-active-turn-even-when-clearing-all.json | 34 +- ...truncated-is-no-op-for-unknown-turnid.json | 34 +- ...w-with-tool-calls-and-re-confirmation.json | 66 +- ...n-isreadchanged-marks-session-as-read.json | 4 +- ...isreadchanged-marks-session-as-unread.json | 4 +- ...ivedchanged-marks-session-as-archived.json | 4 +- ...-isarchivedchanged-unarchives-session.json | 4 +- .../075-turnstarted-clears-isread.json | 34 +- ...olcall-contentchanged-updates-running.json | 34 +- ...lcall-contentchanged-noop-non-running.json | 34 +- ...call-contentchanged-replaces-existing.json | 34 +- ...-session-unknown-action-type-is-no-op.json | 4 +- ...0-toolcalldelta-wrong-turnid-is-no-op.json | 34 +- ...91-responsepart-wrong-turnid-is-no-op.json | 34 +- ...2-toolcallstart-wrong-turnid-is-no-op.json | 34 +- .../093-usage-wrong-turnid-is-no-op.json | 34 +- ...094-delta-nonexistent-partid-is-no-op.json | 34 +- ...5-toolcalldelta-wrong-status-is-no-op.json | 34 +- ...olcallconfirmed-wrong-status-is-no-op.json | 34 +- ...oolcallcomplete-wrong-status-is-no-op.json | 34 +- ...resultconfirmed-wrong-status-is-no-op.json | 34 +- ...dturn-force-cancels-running-tool-call.json | 34 +- ...ta-targeting-toolcall-partid-is-no-op.json | 34 +- ...olcalldelta-without-invocationmessage.json | 34 +- ...ning-targeting-non-reasoning-is-no-op.json | 34 +- .../103-delta-skips-parts-without-id.json | 34 +- ...on-input-full-draft-and-complete-flow.json | 42 +- ...on-input-requested-with-drafts-status.json | 36 +- ...nput-turn-end-cleans-turn-scoped-only.json | 34 +- ...session-input-upsert-and-clear-answer.json | 38 +- ...ssion-input-unknown-actions-are-no-op.json | 36 +- ...t-completion-and-truncation-filtering.json | 36 +- ...confirmation-sets-input-needed-status.json | 36 +- ...confirmation-sets-input-needed-status.json | 38 +- ...nfigchanged-merges-into-config-values.json | 4 +- ...igchanged-noops-when-config-undefined.json | 4 +- ...session-modelchanged-with-modelconfig.json | 4 +- ...olcallready-with-confirmation-options.json | 36 +- ...onfirmed-approved-with-selectedoption.json | 38 +- ...igchanged-replace-replaces-all-values.json | 4 +- ...lconfirmed-denied-with-selectedoption.json | 38 +- ...edoption-carries-through-to-completed.json | 40 +- ...n-carries-through-result-confirmation.json | 42 +- ...doption-carries-through-result-denied.json | 42 +- ...session-activitychanged-sets-activity.json | 4 +- ...ssion-activitychanged-clears-activity.json | 4 +- .../135-session-metachanged-sets-meta.json | 8 +- ...onupdated-replaces-existing-container.json | 30 +- ...stomizationupdated-appends-unknown-id.json | 16 +- ...ion-customizationupdated-creates-list.json | 6 +- ...sion-changesetschanged-sets-catalogue.json | 6 +- ...on-changesetschanged-clears-catalogue.json | 6 +- .../147-session-agentchanged-sets-agent.json | 4 +- ...ion-ready-preserves-inprogress-status.json | 26 +- ...148-session-agentchanged-clears-agent.json | 4 +- ...9-session-agentchanged-replaces-agent.json | 4 +- ...emoved-removes-container-and-children.json | 8 +- ...on-customizationremoved-removes-child.json | 8 +- ...-customizationremoved-noop-unknown-id.json | 8 +- .../156-session-default-chat-changed.json | 36 + ...th-editedtoolinput-overrides-original.json | 40 +- ...statechanged-upserts-top-level-server.json | 16 +- ...0-session-default-chat-changed-unsets.json | 35 + ...rstatechanged-upserts-container-child.json | 16 +- .../161-chat-turn-lifecycle-on-chat.json | 76 + ...mcpserverstatechanged-noop-unknown-id.json | 16 +- ...mcpserverstatechanged-noop-non-mcp-id.json | 12 +- ...ies-mcp-contributor-through-lifecycle.json | 40 +- .../170-session-chatadded-appends.json | 48 + .../171-session-chatadded-upserts.json | 56 + .../reducers/172-session-chatremoved.json | 60 + .../reducers/173-session-chatupdated.json | 56 + .../174-session-chatremoved-noop.json | 52 + .../175-session-chatupdated-noop.json | 51 + .../220-toolcall-actions-update-meta.json | 80 +- types/version/message-checks.ts | 2 + types/version/registry.ts | 48 +- 220 files changed, 20850 insertions(+), 18046 deletions(-) create mode 100644 docs/specification/chat-channel.md create mode 100644 types/channels-chat/actions.ts create mode 100644 types/channels-chat/commands.ts create mode 100644 types/channels-chat/reducer.ts create mode 100644 types/channels-chat/state.ts create mode 100644 types/test-cases/reducers/156-session-default-chat-changed.json create mode 100644 types/test-cases/reducers/160-session-default-chat-changed-unsets.json create mode 100644 types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json create mode 100644 types/test-cases/reducers/170-session-chatadded-appends.json create mode 100644 types/test-cases/reducers/171-session-chatadded-upserts.json create mode 100644 types/test-cases/reducers/172-session-chatremoved.json create mode 100644 types/test-cases/reducers/173-session-chatupdated.json create mode 100644 types/test-cases/reducers/174-session-chatremoved-noop.json create mode 100644 types/test-cases/reducers/175-session-chatupdated-noop.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b92744..c437b0f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,11 +47,26 @@ Spec version: `0.4.0` Resolving or re-anchoring an annotation no longer requires replacing the whole annotation via `annotations/set`. Omitted fields are left unchanged; the annotation's `entries`, `id`, and `_meta` are never touched. +- `ahp-chat:` channel for per-chat conversation state; `SessionState.chats[]` catalog; `SessionState.defaultChat?` input-routing hint; `ChatOrigin` provenance union; `createChat` command. +- `ChatSummary.workingDirectory?` — optional per-chat working directory. When absent, chats inherit the session's `workingDirectory`. Enables agent-swarm patterns where multiple chats in one session operate on independent worktrees. +- Three discrete chat-catalog actions on the session channel — `session/chatAdded` (upsert by `summary.resource`), `session/chatRemoved`, and `session/chatUpdated` (partial-update with `Partial`) — mirroring the root-channel `root/sessionAdded` / `root/sessionRemoved` / `root/sessionSummaryChanged` pattern. - `RootState` now carries an optional `_meta` property bag for implementation-defined metadata about the agent host itself, mirroring the MCP `_meta` convention. A well-known `hostBuild` key may carry build information (version, commit, date) about the program hosting the agent host. +### Changed + +- `fetchTurns` and `completions` now target an `ahp-chat:` channel; `PROTOCOL_VERSION` bumped to `0.4.0`. +- `ChatState` is now **flat** — the previous `summary: ChatSummary` sub-object has been replaced by inlined `resource` / `title` / `status` / `activity` / `modifiedAt` / `model` / `agent` / `origin` / `workingDirectory` fields. `ChatSummary` remains as the standalone catalog entry on `SessionState.chats`. +- `ChatSummary.modifiedAt` and `ChatState.modifiedAt` are now ISO 8601 strings instead of numeric milliseconds. +- `SessionSummary` now documents how its aggregate fields (`status`, `activity`, `modifiedAt`) are derived from the session's chats, including `InputNeeded` / `Error` promotion when any chat raises the flag. + +### Removed + +- `SessionState.turns`, `SessionState.activeTurn`, `SessionState.steeringMessage`, `SessionState.queuedMessages`, `SessionState.inputRequests` (moved to `ChatState`). +- `session/chatsChanged` full-replacement action (replaced by `session/chatAdded` / `session/chatRemoved` / `session/chatUpdated`). + ## [0.3.0] — 2026-06-05 Spec version: `0.3.0` diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index d40b650a..dd4c5fff 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -33,13 +33,23 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. resending its entries. Handled by the annotations reducer (no-op on unknown id). -### Added - +- `ahp-chat:` channel for per-chat conversation state; `SessionState.chats[]` catalog; `SessionState.defaultChat?` input-routing hint; `ChatOrigin` provenance union; `createChat` / `disposeChat` commands. +- `ChatSummary.WorkingDirectory` — optional per-chat working directory. Falls back to the session's `WorkingDirectory` when absent. +- Three discrete chat-catalog actions on the session channel — `SessionChatAddedAction` (upsert by `Summary.Resource`), `SessionChatRemovedAction`, and `SessionChatUpdatedAction` (partial-update payload). - `RootState` now exposes an optional `_meta` property bag (`Meta map[string]json.RawMessage`) for implementation-defined agent-host metadata, such as a well-known `hostBuild` key carrying the host's build version/commit/date. +### Changed + +- `ChatState` is now flat — the previous embedded `Summary` has been replaced with inlined `Resource` / `Title` / `Status` / `Activity` / `ModifiedAt` / `Model` / `Agent` / `Origin` / `WorkingDirectory` fields. `ChatSummary` remains as the standalone catalog entry on `SessionState.Chats`. +- `ChatSummary.ModifiedAt` and `ChatState.ModifiedAt` are now ISO 8601 `string` values instead of integer milliseconds. + +### Removed + +- `SessionChatsChangedAction` (replaced by the three discrete chat-catalog actions above). + ## [0.3.0] — 2026-06-05 Implements AHP 0.3.0. @@ -85,11 +95,14 @@ Implements AHP 0.3.0. ### Changed +- Reducers split into per-chat and session-aggregate handlers to match the multi-chat protocol shape. +- `fetchTurns` and `completions` now target an `ahp-chat:` channel; `PROTOCOL_VERSION` bumped to `0.4.0`. - Renamed the `ChangesetSummary` type to `Changeset`. The on-the-wire shape is unchanged. - Moved the `changesets` catalogue from `SessionSummary` to `SessionState`. The `session/changesetsChanged` action now updates `state.changesets` directly instead of `state.summary.changesets`. ### Removed +- `SessionState.turns`, `SessionState.activeTurn`, `SessionState.steeringMessage`, `SessionState.queuedMessages`, `SessionState.inputRequests` (moved to `ChatState`). - Removed the `additions`, `deletions`, and `files` fields from `ChangesetSummary`. Aggregate counts now live on `SessionSummary.changes`; per-changeset views derive their own totals from `ChangesetState.files`. ### Changed diff --git a/clients/go/ahp/multi_host_state_mirror.go b/clients/go/ahp/multi_host_state_mirror.go index d0a90705..662d57a8 100644 --- a/clients/go/ahp/multi_host_state_mirror.go +++ b/clients/go/ahp/multi_host_state_mirror.go @@ -22,12 +22,13 @@ type HostedResourceKey struct { // The mirror has no opinion about how snapshots are kept in sync with // the server — that's the consumer's job, typically by feeding action // envelopes from a [HostSubscriptionEvent] stream through the matching -// [ApplyActionToRoot] / [ApplyActionToSession] / [ApplyActionToTerminal] -// reducer and re-storing the result. +// [ApplyActionToRoot] / [ApplyActionToSession] / [ApplyActionToChat] / +// [ApplyActionToTerminal] reducer and re-storing the result. type MultiHostStateMirror struct { mu sync.RWMutex roots map[string]ahptypes.RootState session map[HostedResourceKey]ahptypes.SessionState + chat map[HostedResourceKey]ahptypes.ChatState term map[HostedResourceKey]ahptypes.TerminalState changes map[HostedResourceKey]ahptypes.ChangesetState } @@ -37,6 +38,7 @@ func NewMultiHostStateMirror() *MultiHostStateMirror { return &MultiHostStateMirror{ roots: make(map[string]ahptypes.RootState), session: make(map[HostedResourceKey]ahptypes.SessionState), + chat: make(map[HostedResourceKey]ahptypes.ChatState), term: make(map[HostedResourceKey]ahptypes.TerminalState), changes: make(map[HostedResourceKey]ahptypes.ChangesetState), } @@ -74,6 +76,22 @@ func (m *MultiHostStateMirror) Session(hostID string, uri ahptypes.URI) (ahptype return v, ok } +// PutChat stores a chat snapshot under (hostID, uri). +func (m *MultiHostStateMirror) PutChat(hostID string, uri ahptypes.URI, c ahptypes.ChatState) { + m.mu.Lock() + defer m.mu.Unlock() + m.chat[HostedResourceKey{hostID, uri}] = c +} + +// Chat returns the chat snapshot at (hostID, uri), or +// (zero, false) if none is recorded. +func (m *MultiHostStateMirror) Chat(hostID string, uri ahptypes.URI) (ahptypes.ChatState, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + v, ok := m.chat[HostedResourceKey{hostID, uri}] + return v, ok +} + // PutTerminal stores a terminal snapshot under (hostID, uri). func (m *MultiHostStateMirror) PutTerminal(hostID string, uri ahptypes.URI, t ahptypes.TerminalState) { m.mu.Lock() @@ -117,6 +135,11 @@ func (m *MultiHostStateMirror) DropHost(hostID string) { delete(m.session, k) } } + for k := range m.chat { + if k.HostID == hostID { + delete(m.chat, k) + } + } for k := range m.term { if k.HostID == hostID { delete(m.term, k) @@ -136,6 +159,7 @@ func (m *MultiHostStateMirror) DropResource(hostID string, uri ahptypes.URI) { defer m.mu.Unlock() k := HostedResourceKey{hostID, uri} delete(m.session, k) + delete(m.chat, k) delete(m.term, k) delete(m.changes, k) } diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 70e35df1..8ff674ea 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -33,9 +33,10 @@ var ( nowProvider func() int64 = func() int64 { return time.Now().UnixMilli() } ) -// SetNowProvider overrides the function reducers call to stamp -// `summary.modifiedAt`. Useful for tests that need deterministic -// output. Pass nil to restore the default ([time.Now].UnixMilli). +// SetNowProvider overrides the clock reducers use to stamp modifiedAt fields. +// Chat modifiedAt values are formatted as ISO 8601 strings from this clock; +// session summary modifiedAt keeps the numeric millisecond timestamp. Pass nil +// to restore the default ([time.Now].UnixMilli). func SetNowProvider(fn func() int64) { nowMu.Lock() defer nowMu.Unlock() @@ -52,6 +53,10 @@ func nowMs() int64 { return nowProvider() } +func nowISOString() string { + return time.UnixMilli(nowMs()).UTC().Format("2006-01-02T15:04:05.000Z") +} + // ─── Status helpers ──────────────────────────────────────────────────── // statusActivityMask covers the mutually-exclusive activity bits @@ -120,7 +125,7 @@ func toolCallID(tc ahptypes.ToolCallState) string { return toolCallMeta(tc).id } -func hasPendingToolCallConfirmation(state *ahptypes.SessionState) bool { +func hasPendingToolCallConfirmation(state *ahptypes.ChatState) bool { if state.ActiveTurn == nil { return false } @@ -138,7 +143,7 @@ func hasPendingToolCallConfirmation(state *ahptypes.SessionState) bool { return false } -func summaryStatus(state *ahptypes.SessionState, terminal *ahptypes.SessionStatus) ahptypes.SessionStatus { +func summaryStatus(state *ahptypes.ChatState, terminal *ahptypes.SessionStatus) ahptypes.SessionStatus { var activity ahptypes.SessionStatus switch { case terminal != nil: @@ -150,20 +155,24 @@ func summaryStatus(state *ahptypes.SessionState, terminal *ahptypes.SessionStatu default: activity = ahptypes.SessionStatusIdle } - return (state.Summary.Status &^ statusActivityMask) | activity + return (state.Status &^ statusActivityMask) | activity } -func refreshSummaryStatus(state *ahptypes.SessionState) { - state.Summary.Status = summaryStatus(state, nil) +func refreshSummaryStatus(state *ahptypes.ChatState) { + state.Status = summaryStatus(state, nil) } -func touchModified(state *ahptypes.SessionState) { +func touchSessionModified(state *ahptypes.SessionState) { state.Summary.ModifiedAt = nowMs() } +func touchChatModified(state *ahptypes.ChatState) { + state.ModifiedAt = nowISOString() +} + // ─── Active-turn helpers ─────────────────────────────────────────────── -func endTurn(state *ahptypes.SessionState, turnID string, turnState ahptypes.TurnState, terminalStatus *ahptypes.SessionStatus, errInfo *ahptypes.ErrorInfo) ReduceOutcome { +func endTurn(state *ahptypes.ChatState, turnID string, turnState ahptypes.TurnState, terminalStatus *ahptypes.SessionStatus, errInfo *ahptypes.ErrorInfo) ReduceOutcome { if state.ActiveTurn == nil || state.ActiveTurn.Id != turnID { return ReduceOutcomeNoOp } @@ -212,12 +221,12 @@ func endTurn(state *ahptypes.SessionState, turnID string, turnState ahptypes.Tur state.Turns = append(state.Turns, turn) state.InputRequests = nil - touchModified(state) - state.Summary.Status = summaryStatus(state, terminalStatus) + touchChatModified(state) + state.Status = summaryStatus(state, terminalStatus) return ReduceOutcomeApplied } -func upsertInputRequest(state *ahptypes.SessionState, req ahptypes.SessionInputRequest) { +func upsertInputRequest(state *ahptypes.ChatState, req ahptypes.ChatInputRequest) { existing := state.InputRequests found := -1 for i := range existing { @@ -235,9 +244,9 @@ func upsertInputRequest(state *ahptypes.SessionState, req ahptypes.SessionInputR existing = append(existing, req) } state.InputRequests = existing - state.Summary.Status = summaryStatus(state, nil) - touchModified(state) - state.Summary.Status = withStatusFlag(state.Summary.Status, ahptypes.SessionStatusIsRead, false) + state.Status = summaryStatus(state, nil) + touchChatModified(state) + state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsRead, false) } // ─── Customization helpers ───────────────────────────────────────────── @@ -306,7 +315,7 @@ func applyToggle(list []ahptypes.Customization, id string, enabled bool) bool { // ─── Active-turn mutation helpers ────────────────────────────────────── -func updateToolCall(state *ahptypes.SessionState, turnID, targetToolCallID string, updater func(ahptypes.ToolCallState) ahptypes.ToolCallState) ReduceOutcome { +func updateToolCall(state *ahptypes.ChatState, turnID, targetToolCallID string, updater func(ahptypes.ToolCallState) ahptypes.ToolCallState) ReduceOutcome { if state.ActiveTurn == nil || state.ActiveTurn.Id != turnID { return ReduceOutcomeNoOp } @@ -323,7 +332,7 @@ func updateToolCall(state *ahptypes.SessionState, turnID, targetToolCallID strin return ReduceOutcomeNoOp } -func updateResponsePart(state *ahptypes.SessionState, turnID, partID string, updater func(*ahptypes.ResponsePart)) ReduceOutcome { +func updateResponsePart(state *ahptypes.ChatState, turnID, partID string, updater func(*ahptypes.ResponsePart)) ReduceOutcome { if state.ActiveTurn == nil || state.ActiveTurn.Id != turnID { return ReduceOutcomeNoOp } @@ -381,44 +390,36 @@ func ApplyActionToRoot(state *ahptypes.RootState, action ahptypes.StateAction) R return ReduceOutcomeOutOfScope } -// ─── Session Reducer ─────────────────────────────────────────────────── +// ─── Chat Reducer ────────────────────────────────────────────────────── -// ApplyActionToSession applies action to the [ahptypes.SessionState] -// in place. Returns [ReduceOutcomeOutOfScope] for actions that target -// a different state tree. -func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAction) ReduceOutcome { +// ApplyActionToChat applies action to the [ahptypes.ChatState] in +// place. Returns [ReduceOutcomeOutOfScope] for actions that target a +// different state tree. +func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) ReduceOutcome { switch a := action.Value.(type) { - case *ahptypes.SessionReadyAction: - state.Lifecycle = ahptypes.SessionLifecycleReady - return ReduceOutcomeApplied - case *ahptypes.SessionCreationFailedAction: - state.Lifecycle = ahptypes.SessionLifecycleCreationFailed - errCopy := a.Error - state.CreationError = &errCopy - return ReduceOutcomeApplied - case *ahptypes.SessionTurnStartedAction: + case *ahptypes.ChatTurnStartedAction: return applyTurnStarted(state, a) - case *ahptypes.SessionDeltaAction: + case *ahptypes.ChatDeltaAction: return updateResponsePart(state, a.TurnId, a.PartId, func(p *ahptypes.ResponsePart) { if m, ok := p.Value.(*ahptypes.MarkdownResponsePart); ok { m.Content += a.Content } }) - case *ahptypes.SessionResponsePartAction: + case *ahptypes.ChatResponsePartAction: if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId { return ReduceOutcomeNoOp } state.ActiveTurn.ResponseParts = append(state.ActiveTurn.ResponseParts, a.Part) return ReduceOutcomeApplied - case *ahptypes.SessionTurnCompleteAction: + case *ahptypes.ChatTurnCompleteAction: return endTurn(state, a.TurnId, ahptypes.TurnStateComplete, nil, nil) - case *ahptypes.SessionTurnCancelledAction: + case *ahptypes.ChatTurnCancelledAction: return endTurn(state, a.TurnId, ahptypes.TurnStateCancelled, nil, nil) - case *ahptypes.SessionErrorAction: + case *ahptypes.ChatErrorAction: errCopy := a.Error errStatus := ahptypes.SessionStatusError return endTurn(state, a.TurnId, ahptypes.TurnStateError, &errStatus, &errCopy) - case *ahptypes.SessionToolCallStartAction: + case *ahptypes.ChatToolCallStartAction: if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId { return ReduceOutcomeNoOp } @@ -434,33 +435,33 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct }}, }}) return ReduceOutcomeApplied - case *ahptypes.SessionToolCallDeltaAction: + case *ahptypes.ChatToolCallDeltaAction: return applyToolCallDelta(state, a) - case *ahptypes.SessionToolCallReadyAction: + case *ahptypes.ChatToolCallReadyAction: res := applyToolCallReady(state, a) if res == ReduceOutcomeApplied { refreshSummaryStatus(state) } return res - case *ahptypes.SessionToolCallConfirmedAction: + case *ahptypes.ChatToolCallConfirmedAction: res := applyToolCallConfirmed(state, a) if res == ReduceOutcomeApplied { refreshSummaryStatus(state) } return res - case *ahptypes.SessionToolCallCompleteAction: + case *ahptypes.ChatToolCallCompleteAction: res := applyToolCallComplete(state, a) if res == ReduceOutcomeApplied { refreshSummaryStatus(state) } return res - case *ahptypes.SessionToolCallResultConfirmedAction: + case *ahptypes.ChatToolCallResultConfirmedAction: res := applyToolCallResultConfirmed(state, a) if res == ReduceOutcomeApplied { refreshSummaryStatus(state) } return res - case *ahptypes.SessionToolCallContentChangedAction: + case *ahptypes.ChatToolCallContentChangedAction: return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { if r, ok := tc.Value.(*ahptypes.ToolCallRunningState); ok { if a.Meta != nil { @@ -470,31 +471,223 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct } return tc }) - case *ahptypes.SessionTitleChangedAction: - state.Summary.Title = a.Title - touchModified(state) - return ReduceOutcomeApplied - case *ahptypes.SessionUsageAction: + case *ahptypes.ChatUsageAction: if state.ActiveTurn == nil || state.ActiveTurn.Id != a.TurnId { return ReduceOutcomeNoOp } usage := a.Usage state.ActiveTurn.Usage = &usage return ReduceOutcomeApplied - case *ahptypes.SessionReasoningAction: + case *ahptypes.ChatReasoningAction: return updateResponsePart(state, a.TurnId, a.PartId, func(p *ahptypes.ResponsePart) { if r, ok := p.Value.(*ahptypes.ReasoningResponsePart); ok { r.Content += a.Content } }) + case *ahptypes.ChatTruncatedAction: + return applyTruncated(state, a.TurnId) + case *ahptypes.ChatInputRequestedAction: + upsertInputRequest(state, a.Request) + return ReduceOutcomeApplied + case *ahptypes.ChatInputAnswerChangedAction: + return applyInputAnswerChanged(state, a) + case *ahptypes.ChatInputCompletedAction: + list := state.InputRequests + if list == nil { + return ReduceOutcomeNoOp + } + had := false + next := list[:0] + for _, r := range list { + if r.Id == a.RequestId { + had = true + continue + } + next = append(next, r) + } + if !had { + return ReduceOutcomeNoOp + } + if len(next) == 0 { + state.InputRequests = nil + } else { + state.InputRequests = next + } + refreshSummaryStatus(state) + touchChatModified(state) + return ReduceOutcomeApplied + case *ahptypes.ChatPendingMessageSetAction: + entry := ahptypes.PendingMessage{Id: a.Id, Message: a.Message} + switch a.Kind { + case ahptypes.PendingMessageKindSteering: + state.SteeringMessage = &entry + case ahptypes.PendingMessageKindQueued: + list := state.QueuedMessages + idx := -1 + for i := range list { + if list[i].Id == entry.Id { + idx = i + break + } + } + if idx >= 0 { + list[idx] = entry + } else { + list = append(list, entry) + } + state.QueuedMessages = list + } + return ReduceOutcomeApplied + case *ahptypes.ChatPendingMessageRemovedAction: + switch a.Kind { + case ahptypes.PendingMessageKindSteering: + if state.SteeringMessage != nil && state.SteeringMessage.Id == a.Id { + state.SteeringMessage = nil + return ReduceOutcomeApplied + } + return ReduceOutcomeNoOp + case ahptypes.PendingMessageKindQueued: + list := state.QueuedMessages + if list == nil { + return ReduceOutcomeNoOp + } + next := list[:0] + removed := false + for _, m := range list { + if m.Id == a.Id { + removed = true + continue + } + next = append(next, m) + } + if !removed { + return ReduceOutcomeNoOp + } + if len(next) == 0 { + state.QueuedMessages = nil + } else { + state.QueuedMessages = next + } + return ReduceOutcomeApplied + } + return ReduceOutcomeNoOp + case *ahptypes.ChatQueuedMessagesReorderedAction: + if state.QueuedMessages == nil { + return ReduceOutcomeNoOp + } + byID := make(map[string]ahptypes.PendingMessage, len(state.QueuedMessages)) + for _, m := range state.QueuedMessages { + byID[m.Id] = m + } + reordered := make([]ahptypes.PendingMessage, 0, len(byID)) + seen := make(map[string]struct{}, len(byID)) + for _, id := range a.Order { + if msg, ok := byID[id]; ok { + if _, dup := seen[id]; !dup { + seen[id] = struct{}{} + reordered = append(reordered, msg) + } + } + } + // Append messages absent from `order`, preserving their original + // relative order in state.QueuedMessages (mirrors the canonical + // TypeScript reducer in types/channels-session/reducer.ts). + for _, m := range state.QueuedMessages { + if _, in := seen[m.Id]; !in { + reordered = append(reordered, m) + } + } + state.QueuedMessages = reordered + return ReduceOutcomeApplied + } + return ReduceOutcomeOutOfScope +} + +func mergeChatSummaryPartial(summary *ahptypes.ChatSummary, changes ahptypes.PartialChatSummary) { + if changes.Title != nil { + summary.Title = *changes.Title + } + if changes.Status != nil { + summary.Status = *changes.Status + } + if changes.Activity != nil { + summary.Activity = changes.Activity + } + if changes.ModifiedAt != nil { + summary.ModifiedAt = *changes.ModifiedAt + } + if changes.Model != nil { + summary.Model = changes.Model + } + if changes.Agent != nil { + summary.Agent = changes.Agent + } + if changes.Origin != nil { + summary.Origin = changes.Origin + } + if changes.WorkingDirectory != nil { + summary.WorkingDirectory = changes.WorkingDirectory + } +} + +// ─── Session Reducer ─────────────────────────────────────────────────── + +// ApplyActionToSession applies action to the [ahptypes.SessionState] +// in place. Returns [ReduceOutcomeOutOfScope] for actions that target +// a different state tree. +func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAction) ReduceOutcome { + switch a := action.Value.(type) { + case *ahptypes.SessionReadyAction: + state.Lifecycle = ahptypes.SessionLifecycleReady + return ReduceOutcomeApplied + case *ahptypes.SessionCreationFailedAction: + state.Lifecycle = ahptypes.SessionLifecycleCreationFailed + errCopy := a.Error + state.CreationError = &errCopy + return ReduceOutcomeApplied + case *ahptypes.SessionChatAddedAction: + for i := range state.Chats { + if state.Chats[i].Resource == a.Summary.Resource { + state.Chats[i] = a.Summary + return ReduceOutcomeApplied + } + } + state.Chats = append(state.Chats, a.Summary) + return ReduceOutcomeApplied + case *ahptypes.SessionChatRemovedAction: + for i := range state.Chats { + if state.Chats[i].Resource == a.Chat { + state.Chats = append(state.Chats[:i], state.Chats[i+1:]...) + if state.DefaultChat != nil && *state.DefaultChat == a.Chat { + state.DefaultChat = nil + } + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp + case *ahptypes.SessionChatUpdatedAction: + for i := range state.Chats { + if state.Chats[i].Resource == a.Chat { + mergeChatSummaryPartial(&state.Chats[i], a.Changes) + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp + case *ahptypes.SessionDefaultChatChangedAction: + state.DefaultChat = a.DefaultChat + return ReduceOutcomeApplied + case *ahptypes.SessionTitleChangedAction: + state.Summary.Title = a.Title + touchSessionModified(state) + return ReduceOutcomeApplied case *ahptypes.SessionModelChangedAction: model := a.Model state.Summary.Model = &model - touchModified(state) + touchSessionModified(state) return ReduceOutcomeApplied case *ahptypes.SessionAgentChangedAction: state.Summary.Agent = a.Agent - touchModified(state) + touchSessionModified(state) return ReduceOutcomeApplied case *ahptypes.SessionIsReadChangedAction: state.Summary.Status = withStatusFlag(state.Summary.Status, ahptypes.SessionStatusIsRead, a.IsRead) @@ -525,7 +718,7 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct for k, v := range a.Config { state.Config.Values[k] = v } - touchModified(state) + touchSessionModified(state) return ReduceOutcomeApplied case *ahptypes.SessionMetaChangedAction: state.Meta = a.Meta @@ -599,134 +792,19 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct return ReduceOutcomeNoOp case *ahptypes.SessionMcpServerStateChangedAction: return applyMcpServerStatusChanged(state, a) - case *ahptypes.SessionTruncatedAction: - return applyTruncated(state, a.TurnId) - case *ahptypes.SessionInputRequestedAction: - upsertInputRequest(state, a.Request) - return ReduceOutcomeApplied - case *ahptypes.SessionInputAnswerChangedAction: - return applyInputAnswerChanged(state, a) - case *ahptypes.SessionInputCompletedAction: - list := state.InputRequests - if list == nil { - return ReduceOutcomeNoOp - } - had := false - next := list[:0] - for _, r := range list { - if r.Id == a.RequestId { - had = true - continue - } - next = append(next, r) - } - if !had { - return ReduceOutcomeNoOp - } - if len(next) == 0 { - state.InputRequests = nil - } else { - state.InputRequests = next - } - refreshSummaryStatus(state) - touchModified(state) - return ReduceOutcomeApplied - case *ahptypes.SessionPendingMessageSetAction: - entry := ahptypes.PendingMessage{Id: a.Id, Message: a.Message} - switch a.Kind { - case ahptypes.PendingMessageKindSteering: - state.SteeringMessage = &entry - case ahptypes.PendingMessageKindQueued: - list := state.QueuedMessages - idx := -1 - for i := range list { - if list[i].Id == entry.Id { - idx = i - break - } - } - if idx >= 0 { - list[idx] = entry - } else { - list = append(list, entry) - } - state.QueuedMessages = list - } - return ReduceOutcomeApplied - case *ahptypes.SessionPendingMessageRemovedAction: - switch a.Kind { - case ahptypes.PendingMessageKindSteering: - if state.SteeringMessage != nil && state.SteeringMessage.Id == a.Id { - state.SteeringMessage = nil - return ReduceOutcomeApplied - } - return ReduceOutcomeNoOp - case ahptypes.PendingMessageKindQueued: - list := state.QueuedMessages - if list == nil { - return ReduceOutcomeNoOp - } - next := list[:0] - removed := false - for _, m := range list { - if m.Id == a.Id { - removed = true - continue - } - next = append(next, m) - } - if !removed { - return ReduceOutcomeNoOp - } - if len(next) == 0 { - state.QueuedMessages = nil - } else { - state.QueuedMessages = next - } - return ReduceOutcomeApplied - } - return ReduceOutcomeNoOp - case *ahptypes.SessionQueuedMessagesReorderedAction: - if state.QueuedMessages == nil { - return ReduceOutcomeNoOp - } - byID := make(map[string]ahptypes.PendingMessage, len(state.QueuedMessages)) - for _, m := range state.QueuedMessages { - byID[m.Id] = m - } - reordered := make([]ahptypes.PendingMessage, 0, len(byID)) - seen := make(map[string]struct{}, len(byID)) - for _, id := range a.Order { - if msg, ok := byID[id]; ok { - if _, dup := seen[id]; !dup { - seen[id] = struct{}{} - reordered = append(reordered, msg) - } - } - } - // Append messages absent from `order`, preserving their original - // relative order in state.QueuedMessages (mirrors the canonical - // TypeScript reducer in types/channels-session/reducer.ts). - for _, m := range state.QueuedMessages { - if _, in := seen[m.Id]; !in { - reordered = append(reordered, m) - } - } - state.QueuedMessages = reordered - return ReduceOutcomeApplied } return ReduceOutcomeOutOfScope } -func applyTurnStarted(state *ahptypes.SessionState, a *ahptypes.SessionTurnStartedAction) ReduceOutcome { +func applyTurnStarted(state *ahptypes.ChatState, a *ahptypes.ChatTurnStartedAction) ReduceOutcome { state.ActiveTurn = &ahptypes.ActiveTurn{ Id: a.TurnId, Message: a.Message, ResponseParts: []ahptypes.ResponsePart{}, } - state.Summary.Status = summaryStatus(state, nil) - touchModified(state) - state.Summary.Status = withStatusFlag(state.Summary.Status, ahptypes.SessionStatusIsRead, false) + state.Status = summaryStatus(state, nil) + touchChatModified(state) + state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsRead, false) if a.QueuedMessageId != nil { qmid := *a.QueuedMessageId @@ -751,7 +829,7 @@ func applyTurnStarted(state *ahptypes.SessionState, a *ahptypes.SessionTurnStart return ReduceOutcomeApplied } -func applyToolCallDelta(state *ahptypes.SessionState, a *ahptypes.SessionToolCallDeltaAction) ReduceOutcome { +func applyToolCallDelta(state *ahptypes.ChatState, a *ahptypes.ChatToolCallDeltaAction) ReduceOutcome { return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { s, ok := tc.Value.(*ahptypes.ToolCallStreamingState) if !ok { @@ -774,7 +852,7 @@ func applyToolCallDelta(state *ahptypes.SessionState, a *ahptypes.SessionToolCal }) } -func applyToolCallReady(state *ahptypes.SessionState, a *ahptypes.SessionToolCallReadyAction) ReduceOutcome { +func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReadyAction) ReduceOutcome { return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { common := toolCallMeta(tc) if a.Meta != nil { @@ -827,7 +905,7 @@ func resolveSelectedOption(options []ahptypes.ConfirmationOption, id *string) *a return nil } -func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToolCallConfirmedAction) ReduceOutcome { +func applyToolCallConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToolCallConfirmedAction) ReduceOutcome { return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { s, ok := tc.Value.(*ahptypes.ToolCallPendingConfirmationState) if !ok { @@ -885,7 +963,7 @@ func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToo }) } -func applyToolCallComplete(state *ahptypes.SessionState, a *ahptypes.SessionToolCallCompleteAction) ReduceOutcome { +func applyToolCallComplete(state *ahptypes.ChatState, a *ahptypes.ChatToolCallCompleteAction) ReduceOutcome { return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { common := toolCallMeta(tc) if a.Meta != nil { @@ -949,7 +1027,7 @@ func applyToolCallComplete(state *ahptypes.SessionState, a *ahptypes.SessionTool }) } -func applyToolCallResultConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToolCallResultConfirmedAction) ReduceOutcome { +func applyToolCallResultConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToolCallResultConfirmedAction) ReduceOutcome { return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { s, ok := tc.Value.(*ahptypes.ToolCallPendingResultConfirmationState) if !ok { @@ -1037,7 +1115,7 @@ func applyMcpServerStatusChanged(state *ahptypes.SessionState, a *ahptypes.Sessi return ReduceOutcomeNoOp } -func applyTruncated(state *ahptypes.SessionState, turnID *string) ReduceOutcome { +func applyTruncated(state *ahptypes.ChatState, turnID *string) ReduceOutcome { if turnID == nil { state.Turns = []ahptypes.Turn{} } else { @@ -1055,12 +1133,12 @@ func applyTruncated(state *ahptypes.SessionState, turnID *string) ReduceOutcome } state.ActiveTurn = nil state.InputRequests = nil - touchModified(state) - state.Summary.Status = summaryStatus(state, nil) + touchChatModified(state) + state.Status = summaryStatus(state, nil) return ReduceOutcomeApplied } -func applyInputAnswerChanged(state *ahptypes.SessionState, a *ahptypes.SessionInputAnswerChangedAction) ReduceOutcome { +func applyInputAnswerChanged(state *ahptypes.ChatState, a *ahptypes.ChatInputAnswerChangedAction) ReduceOutcome { list := state.InputRequests idx := -1 for i := range list { @@ -1074,7 +1152,7 @@ func applyInputAnswerChanged(state *ahptypes.SessionState, a *ahptypes.SessionIn } req := &list[idx] if req.Answers == nil { - req.Answers = make(map[string]ahptypes.SessionInputAnswer) + req.Answers = make(map[string]ahptypes.ChatInputAnswer) } if a.Answer == nil { delete(req.Answers, a.QuestionId) @@ -1084,7 +1162,7 @@ func applyInputAnswerChanged(state *ahptypes.SessionState, a *ahptypes.SessionIn if len(req.Answers) == 0 { req.Answers = nil } - touchModified(state) + touchChatModified(state) return ReduceOutcomeApplied } diff --git a/clients/go/ahp/reducers_fixture_test.go b/clients/go/ahp/reducers_fixture_test.go index 0d64fddd..b74bdb9c 100644 --- a/clients/go/ahp/reducers_fixture_test.go +++ b/clients/go/ahp/reducers_fixture_test.go @@ -91,10 +91,11 @@ var reducerFixturesSkipList = map[string]string{ func TestFixtureDrivenReducerParity(t *testing.T) { dir := findFixtureDir(t) - // Use a deterministic timestamp so summary.modifiedAt matches - // what the TypeScript reference reducer stamps in fixtures. - const mockNow int64 = 9999 - SetNowProvider(func() int64 { return mockNow }) + // Use a deterministic timestamp so modifiedAt matches the TypeScript + // reference reducer: Date.now() === 9999, so chat timestamps become + // "1970-01-01T00:00:09.999Z". + const mockNowMillis int64 = 9999 + SetNowProvider(func() int64 { return mockNowMillis }) t.Cleanup(func() { SetNowProvider(nil) }) entries, err := os.ReadDir(dir) @@ -149,6 +150,8 @@ func TestFixtureDrivenReducerParity(t *testing.T) { runFixture[ahptypes.RootState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToRoot) case "session": runFixture[ahptypes.SessionState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToSession) + case "chat": + runFixture[ahptypes.ChatState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToChat) case "terminal": runFixture[ahptypes.TerminalState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToTerminal) case "changeset": diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 6c5a0263..ac0e1417 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -23,39 +23,43 @@ const ( ActionTypeRootActiveSessionsChanged ActionType = "root/activeSessionsChanged" ActionTypeSessionReady ActionType = "session/ready" ActionTypeSessionCreationFailed ActionType = "session/creationFailed" - ActionTypeSessionTurnStarted ActionType = "session/turnStarted" - ActionTypeSessionDelta ActionType = "session/delta" - ActionTypeSessionResponsePart ActionType = "session/responsePart" - ActionTypeSessionToolCallStart ActionType = "session/toolCallStart" - ActionTypeSessionToolCallDelta ActionType = "session/toolCallDelta" - ActionTypeSessionToolCallReady ActionType = "session/toolCallReady" - ActionTypeSessionToolCallConfirmed ActionType = "session/toolCallConfirmed" - ActionTypeSessionToolCallComplete ActionType = "session/toolCallComplete" - ActionTypeSessionToolCallResultConfirmed ActionType = "session/toolCallResultConfirmed" - ActionTypeSessionToolCallContentChanged ActionType = "session/toolCallContentChanged" - ActionTypeSessionTurnComplete ActionType = "session/turnComplete" - ActionTypeSessionTurnCancelled ActionType = "session/turnCancelled" - ActionTypeSessionError ActionType = "session/error" + ActionTypeSessionChatAdded ActionType = "session/chatAdded" + ActionTypeSessionChatRemoved ActionType = "session/chatRemoved" + ActionTypeSessionChatUpdated ActionType = "session/chatUpdated" + ActionTypeSessionDefaultChatChanged ActionType = "session/defaultChatChanged" + ActionTypeChatTurnStarted ActionType = "chat/turnStarted" + ActionTypeChatDelta ActionType = "chat/delta" + ActionTypeChatResponsePart ActionType = "chat/responsePart" + ActionTypeChatToolCallStart ActionType = "chat/toolCallStart" + ActionTypeChatToolCallDelta ActionType = "chat/toolCallDelta" + ActionTypeChatToolCallReady ActionType = "chat/toolCallReady" + ActionTypeChatToolCallConfirmed ActionType = "chat/toolCallConfirmed" + ActionTypeChatToolCallComplete ActionType = "chat/toolCallComplete" + ActionTypeChatToolCallResultConfirmed ActionType = "chat/toolCallResultConfirmed" + ActionTypeChatToolCallContentChanged ActionType = "chat/toolCallContentChanged" + ActionTypeChatTurnComplete ActionType = "chat/turnComplete" + ActionTypeChatTurnCancelled ActionType = "chat/turnCancelled" + ActionTypeChatError ActionType = "chat/error" ActionTypeSessionTitleChanged ActionType = "session/titleChanged" - ActionTypeSessionUsage ActionType = "session/usage" - ActionTypeSessionReasoning ActionType = "session/reasoning" + ActionTypeChatUsage ActionType = "chat/usage" + ActionTypeChatReasoning ActionType = "chat/reasoning" ActionTypeSessionModelChanged ActionType = "session/modelChanged" ActionTypeSessionAgentChanged ActionType = "session/agentChanged" ActionTypeSessionServerToolsChanged ActionType = "session/serverToolsChanged" ActionTypeSessionActiveClientChanged ActionType = "session/activeClientChanged" ActionTypeSessionActiveClientToolsChanged ActionType = "session/activeClientToolsChanged" - ActionTypeSessionPendingMessageSet ActionType = "session/pendingMessageSet" - ActionTypeSessionPendingMessageRemoved ActionType = "session/pendingMessageRemoved" - ActionTypeSessionQueuedMessagesReordered ActionType = "session/queuedMessagesReordered" - ActionTypeSessionInputRequested ActionType = "session/inputRequested" - ActionTypeSessionInputAnswerChanged ActionType = "session/inputAnswerChanged" - ActionTypeSessionInputCompleted ActionType = "session/inputCompleted" + ActionTypeChatPendingMessageSet ActionType = "chat/pendingMessageSet" + ActionTypeChatPendingMessageRemoved ActionType = "chat/pendingMessageRemoved" + ActionTypeChatQueuedMessagesReordered ActionType = "chat/queuedMessagesReordered" + ActionTypeChatInputRequested ActionType = "chat/inputRequested" + ActionTypeChatInputAnswerChanged ActionType = "chat/inputAnswerChanged" + ActionTypeChatInputCompleted ActionType = "chat/inputCompleted" ActionTypeSessionCustomizationsChanged ActionType = "session/customizationsChanged" ActionTypeSessionCustomizationToggled ActionType = "session/customizationToggled" ActionTypeSessionCustomizationUpdated ActionType = "session/customizationUpdated" ActionTypeSessionCustomizationRemoved ActionType = "session/customizationRemoved" ActionTypeSessionMcpServerStateChanged ActionType = "session/mcpServerStateChanged" - ActionTypeSessionTruncated ActionType = "session/truncated" + ActionTypeChatTruncated ActionType = "chat/truncated" ActionTypeSessionIsReadChanged ActionType = "session/isReadChanged" ActionTypeSessionIsArchivedChanged ActionType = "session/isArchivedChanged" ActionTypeSessionActivityChanged ActionType = "session/activityChanged" @@ -148,10 +152,56 @@ type SessionCreationFailedAction struct { Error ErrorInfo `json:"error"` } +// A chat was added to this session's catalog. Upsert semantics: if a chat +// with the same `summary.resource` already exists, the existing entry is +// replaced. +// +// Mirrors the root-channel `root/sessionAdded` notification. +type SessionChatAddedAction struct { + Type ActionType `json:"type"` + // The full summary of the newly added (or upserted) chat. + Summary ChatSummary `json:"summary"` +} + +// A chat was removed from this session's catalog. No-op when no entry matches. +// +// Mirrors the root-channel `root/sessionRemoved` notification. +type SessionChatRemovedAction struct { + Type ActionType `json:"type"` + // The URI of the chat to remove. + Chat URI `json:"chat"` +} + +// One existing chat's summary fields changed. +// +// Partial-update semantics: only fields present in `changes` are written; +// omitted fields are preserved. Identity fields (`resource`) MUST NOT be +// carried in `changes`. No-op when no entry with `chat` exists — clients +// SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. +// +// Mirrors the root-channel `root/sessionSummaryChanged` notification. +type SessionChatUpdatedAction struct { + Type ActionType `json:"type"` + // The URI of the chat whose summary changed. + Chat URI `json:"chat"` + // Mutable summary fields that changed; omitted fields are unchanged. + // + // Identity fields (`resource`) never change and MUST be omitted by + // senders; receivers SHOULD ignore them if present. + Changes PartialChatSummary `json:"changes"` +} + +// The default chat input-routing hint for this session changed. +type SessionDefaultChatChangedAction struct { + Type ActionType `json:"type"` + // New default chat URI, or `undefined` to clear the hint. + DefaultChat *URI `json:"defaultChat,omitempty"` +} + // A new message has been sent to the agent, and a new turn starts. // // A client is only allowed to send {@link MessageKind.User} messages. -type SessionTurnStartedAction struct { +type ChatTurnStartedAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` @@ -163,9 +213,9 @@ type SessionTurnStartedAction struct { // Streaming text chunk from the assistant, appended to a specific response part. // -// The server MUST first emit a `session/responsePart` to create the target +// The server MUST first emit a `chat/responsePart` to create the target // part (markdown or reasoning), then use this action to append text to it. -type SessionDeltaAction struct { +type ChatDeltaAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` @@ -176,7 +226,7 @@ type SessionDeltaAction struct { } // Structured content appended to the response. -type SessionResponsePartAction struct { +type ChatResponsePartAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` @@ -189,9 +239,9 @@ type SessionResponsePartAction struct { // The server sets {@link ToolCallContributor | `contributor`} to identify // the origin of the tool. For client-provided tools, the named client is // responsible for executing the tool once it reaches the `running` state -// and dispatching `session/toolCallComplete`. For MCP-served tools, the +// and dispatching `chat/toolCallComplete`. For MCP-served tools, the // server executes the call against the named `McpServerCustomization`. -type SessionToolCallStartAction struct { +type ChatToolCallStartAction struct { // Turn identifier TurnId string `json:"turnId"` // Tool call identifier @@ -214,7 +264,7 @@ type SessionToolCallStartAction struct { } // Streaming partial parameters for a tool call. -type SessionToolCallDeltaAction struct { +type ChatToolCallDeltaAction struct { // Turn identifier TurnId string `json:"turnId"` // Tool call identifier @@ -241,12 +291,12 @@ type SessionToolCallDeltaAction struct { // When dispatched for a `running` tool call (e.g. mid-execution permission needed), // transitions back to `pending-confirmation`. The `invocationMessage` and `_meta` // SHOULD be updated to describe the specific confirmation needed. Clients use the -// standard `session/toolCallConfirmed` flow to approve or deny. +// standard `chat/toolCallConfirmed` flow to approve or deny. // // For client-provided tools, the server typically sets `confirmed` to // `'not-needed'` so the tool transitions directly to `running`, where the // owning client can begin execution immediately. -type SessionToolCallReadyAction struct { +type ChatToolCallReadyAction struct { // Turn identifier TurnId string `json:"turnId"` // Tool call identifier @@ -278,9 +328,9 @@ type SessionToolCallReadyAction struct { Options []ConfirmationOption `json:"options,omitempty"` } -// SessionToolCallConfirmedAction is the client approves or denies a +// ChatToolCallConfirmedAction is the client approves or denies a // pending tool call (merged approved + denied variants on the wire). -type SessionToolCallConfirmedAction struct { +type ChatToolCallConfirmedAction struct { Type ActionType `json:"type"` TurnId string `json:"turnId"` ToolCallId string `json:"toolCallId"` @@ -304,7 +354,7 @@ type SessionToolCallConfirmedAction struct { // Servers waiting on a client tool call MAY time out after a reasonable duration // if the implementing client disconnects or becomes unresponsive, and dispatch // this action with `result.success = false` and an appropriate error. -type SessionToolCallCompleteAction struct { +type ChatToolCallCompleteAction struct { // Turn identifier TurnId string `json:"turnId"` // Tool call identifier @@ -326,7 +376,7 @@ type SessionToolCallCompleteAction struct { // Client approves or denies a tool's result. // // If `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`. -type SessionToolCallResultConfirmedAction struct { +type ChatToolCallResultConfirmedAction struct { // Turn identifier TurnId string `json:"turnId"` // Tool call identifier @@ -343,22 +393,49 @@ type SessionToolCallResultConfirmedAction struct { Approved bool `json:"approved"` } +// Partial content produced while a tool is still executing. +// +// Replaces the `content` array on the running tool call state. Clients can +// use this to display live feedback (e.g. a terminal reference) before the +// tool completes. +// +// For client-provided tools (where `toolClientId` is set on the tool call state), +// the owning client dispatches this action to stream intermediate content while +// executing. The server SHOULD reject this action if the dispatching client does +// not match `toolClientId`. +type ChatToolCallContentChangedAction struct { + // Turn identifier + TurnId string `json:"turnId"` + // Tool call identifier + ToolCallId string `json:"toolCallId"` + // Additional provider-specific metadata for this tool call. + // + // Clients MAY look for well-known keys here to provide enhanced UI. + // For example, a `ptyTerminal` key with `{ input: string; output: string }` + // indicates the tool operated on a terminal (both `input` and `output` may + // contain escape sequences). + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Type ActionType `json:"type"` + // The current partial content for the running tool call + Content []ToolResultContent `json:"content"` +} + // Turn finished — the assistant is idle. -type SessionTurnCompleteAction struct { +type ChatTurnCompleteAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` } // Turn was aborted; server stops processing. -type SessionTurnCancelledAction struct { +type ChatTurnCancelledAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` } // Error during turn processing. -type SessionErrorAction struct { +type ChatErrorAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` @@ -375,7 +452,7 @@ type SessionTitleChangedAction struct { } // Token usage report for a turn. -type SessionUsageAction struct { +type ChatUsageAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` @@ -385,9 +462,9 @@ type SessionUsageAction struct { // Reasoning/thinking text from the model, appended to a specific reasoning response part. // -// The server MUST first emit a `session/responsePart` to create the target +// The server MUST first emit a `chat/responsePart` to create the target // reasoning part, then use this action to append text to it. -type SessionReasoningAction struct { +type ChatReasoningAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` @@ -397,6 +474,104 @@ type SessionReasoningAction struct { Content string `json:"content"` } +// A pending message was set (upsert semantics: creates or replaces). +// +// For steering messages, this always replaces the single steering message. +// For queued messages, if a message with the given `id` already exists it is +// updated in place; otherwise it is appended to the queue. If the chat is +// idle when a queued message is set, the server SHOULD immediately consume it +// and start a new turn. +// +// A client is only allowed to send {@link MessageKind.User} messages. +type ChatPendingMessageSetAction struct { + Type ActionType `json:"type"` + // Whether this is a steering or queued message + Kind PendingMessageKind `json:"kind"` + // Unique identifier for this pending message + Id string `json:"id"` + // The message content + Message Message `json:"message"` +} + +// A pending message was removed (steering or queued). +// +// Dispatched by clients to cancel a pending message, or by the server when +// it consumes a message (e.g. starting a turn from a queued message or +// injecting a steering message into the current turn). +type ChatPendingMessageRemovedAction struct { + Type ActionType `json:"type"` + // Whether this is a steering or queued message + Kind PendingMessageKind `json:"kind"` + // Identifier of the pending message to remove + Id string `json:"id"` +} + +// Reorder the queued messages. +// +// The `order` array contains the IDs of queued messages in their new +// desired order. IDs not present in the current queue are ignored. +// Queued messages whose IDs are absent from `order` are appended at +// the end in their original relative order (so a client with a stale +// view of the queue never silently drops messages). +type ChatQueuedMessagesReorderedAction struct { + Type ActionType `json:"type"` + // Queued message IDs in the desired order + Order []string `json:"order"` +} + +// A session requested input from the user. +// +// Full-request upsert semantics: the `request` replaces any existing request +// with the same `id`, or is appended if it is new. Answer drafts are preserved +// unless `request.answers` is provided. +type ChatInputRequestedAction struct { + Type ActionType `json:"type"` + // Input request to create or replace + Request ChatInputRequest `json:"request"` +} + +// A client updated, submitted, skipped, or removed a single in-progress answer. +// +// Dispatching with `answer: undefined` removes that question's answer draft. +type ChatInputAnswerChangedAction struct { + Type ActionType `json:"type"` + // Input request identifier + RequestId string `json:"requestId"` + // Question identifier within the input request + QuestionId string `json:"questionId"` + // Updated answer, or `undefined` to clear an answer draft + Answer *ChatInputAnswer `json:"answer,omitempty"` +} + +// A client accepted, declined, or cancelled a session input request. +// +// If accepted, the server uses `answers` (when provided) plus the request's +// synced answer state to resume the blocked operation. +type ChatInputCompletedAction struct { + Type ActionType `json:"type"` + // Input request identifier + RequestId string `json:"requestId"` + // Completion outcome + Response ChatInputResponseKind `json:"response"` + // Optional final answer replacement, keyed by question ID + Answers map[string]ChatInputAnswer `json:"answers,omitempty"` +} + +// Truncates a session's history. If `turnId` is provided, all turns after that +// turn are removed and the specified turn is kept. If `turnId` is omitted, all +// turns are removed. +// +// If there is an active turn it is silently dropped and the chat status +// returns to `idle`. +// +// Common use-case: truncate old data then dispatch a new +// `chat/turnStarted` with an edited message. +type ChatTruncatedAction struct { + Type ActionType `json:"type"` + // Keep turns up to and including this turn. Omit to clear all turns. + TurnId *string `json:"turnId,omitempty"` +} + // Model changed for this session. type SessionModelChangedAction struct { Type ActionType `json:"type"` @@ -497,89 +672,6 @@ type SessionActiveClientToolsChangedAction struct { Tools []ToolDefinition `json:"tools"` } -// A pending message was set (upsert semantics: creates or replaces). -// -// For steering messages, this always replaces the single steering message. -// For queued messages, if a message with the given `id` already exists it is -// updated in place; otherwise it is appended to the queue. If the session is -// idle when a queued message is set, the server SHOULD immediately consume it -// and start a new turn. -// -// A client is only allowed to send {@link MessageKind.User} messages. -type SessionPendingMessageSetAction struct { - Type ActionType `json:"type"` - // Whether this is a steering or queued message - Kind PendingMessageKind `json:"kind"` - // Unique identifier for this pending message - Id string `json:"id"` - // The message content - Message Message `json:"message"` -} - -// A pending message was removed (steering or queued). -// -// Dispatched by clients to cancel a pending message, or by the server when -// it consumes a message (e.g. starting a turn from a queued message or -// injecting a steering message into the current turn). -type SessionPendingMessageRemovedAction struct { - Type ActionType `json:"type"` - // Whether this is a steering or queued message - Kind PendingMessageKind `json:"kind"` - // Identifier of the pending message to remove - Id string `json:"id"` -} - -// Reorder the queued messages. -// -// The `order` array contains the IDs of queued messages in their new -// desired order. IDs not present in the current queue are ignored. -// Queued messages whose IDs are absent from `order` are appended at -// the end in their original relative order (so a client with a stale -// view of the queue never silently drops messages). -type SessionQueuedMessagesReorderedAction struct { - Type ActionType `json:"type"` - // Queued message IDs in the desired order - Order []string `json:"order"` -} - -// A session requested input from the user. -// -// Full-request upsert semantics: the `request` replaces any existing request -// with the same `id`, or is appended if it is new. Answer drafts are preserved -// unless `request.answers` is provided. -type SessionInputRequestedAction struct { - Type ActionType `json:"type"` - // Input request to create or replace - Request SessionInputRequest `json:"request"` -} - -// A client updated, submitted, skipped, or removed a single in-progress answer. -// -// Dispatching with `answer: undefined` removes that question's answer draft. -type SessionInputAnswerChangedAction struct { - Type ActionType `json:"type"` - // Input request identifier - RequestId string `json:"requestId"` - // Question identifier within the input request - QuestionId string `json:"questionId"` - // Updated answer, or `undefined` to clear an answer draft - Answer *SessionInputAnswer `json:"answer,omitempty"` -} - -// A client accepted, declined, or cancelled a session input request. -// -// If accepted, the server uses `answers` (when provided) plus the request's -// synced answer state to resume the blocked operation. -type SessionInputCompletedAction struct { - Type ActionType `json:"type"` - // Input request identifier - RequestId string `json:"requestId"` - // Completion outcome - Response SessionInputResponseKind `json:"response"` - // Optional final answer replacement, keyed by question ID - Answers map[string]SessionInputAnswer `json:"answers,omitempty"` -} - // The session's customizations have changed. // // Full-replacement semantics: the `customizations` array replaces the @@ -661,21 +753,6 @@ type SessionMcpServerStateChangedAction struct { Channel *URI `json:"channel,omitempty"` } -// Truncates a session's history. If `turnId` is provided, all turns after that -// turn are removed and the specified turn is kept. If `turnId` is omitted, all -// turns are removed. -// -// If there is an active turn it is silently dropped and the session status -// returns to `idle`. -// -// Common use-case: truncate old data then dispatch a new -// `session/turnStarted` with an edited message. -type SessionTruncatedAction struct { - Type ActionType `json:"type"` - // Keep turns up to and including this turn. Omit to clear all turns. - TurnId *string `json:"turnId,omitempty"` -} - // Client changed a mutable config value mid-session. // // Only properties with `sessionMutable: true` in the config schema may be @@ -698,33 +775,6 @@ type SessionMetaChangedAction struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` } -// Partial content produced while a tool is still executing. -// -// Replaces the `content` array on the running tool call state. Clients can -// use this to display live feedback (e.g. a terminal reference) before the -// tool completes. -// -// For client-provided tools (where `toolClientId` is set on the tool call state), -// the owning client dispatches this action to stream intermediate content while -// executing. The server SHOULD reject this action if the dispatching client does -// not match `toolClientId`. -type SessionToolCallContentChangedAction struct { - // Turn identifier - TurnId string `json:"turnId"` - // Tool call identifier - ToolCallId string `json:"toolCallId"` - // Additional provider-specific metadata for this tool call. - // - // Clients MAY look for well-known keys here to provide enhanced UI. - // For example, a `ptyTerminal` key with `{ input: string; output: string }` - // indicates the tool operated on a terminal (both `input` and `output` may - // contain escape sequences). - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - Type ActionType `json:"type"` - // The current partial content for the running tool call - Content []ToolResultContent `json:"content"` -} - // The {@link ChangesetState.status} for this changeset transitioned (e.g. // `computing → ready`). The error payload is set together with `status` // whenever it transitions to {@link ChangesetStatus.Error | Error}. @@ -1056,21 +1106,33 @@ func (*RootActiveSessionsChangedAction) isStateAction() {} func (*RootConfigChangedAction) isStateAction() {} func (*SessionReadyAction) isStateAction() {} func (*SessionCreationFailedAction) isStateAction() {} -func (*SessionTurnStartedAction) isStateAction() {} -func (*SessionDeltaAction) isStateAction() {} -func (*SessionResponsePartAction) isStateAction() {} -func (*SessionToolCallStartAction) isStateAction() {} -func (*SessionToolCallDeltaAction) isStateAction() {} -func (*SessionToolCallReadyAction) isStateAction() {} -func (*SessionToolCallConfirmedAction) isStateAction() {} -func (*SessionToolCallCompleteAction) isStateAction() {} -func (*SessionToolCallResultConfirmedAction) isStateAction() {} -func (*SessionTurnCompleteAction) isStateAction() {} -func (*SessionTurnCancelledAction) isStateAction() {} -func (*SessionErrorAction) isStateAction() {} +func (*SessionChatAddedAction) isStateAction() {} +func (*SessionChatRemovedAction) isStateAction() {} +func (*SessionChatUpdatedAction) isStateAction() {} +func (*SessionDefaultChatChangedAction) isStateAction() {} +func (*ChatTurnStartedAction) isStateAction() {} +func (*ChatDeltaAction) isStateAction() {} +func (*ChatResponsePartAction) isStateAction() {} +func (*ChatToolCallStartAction) isStateAction() {} +func (*ChatToolCallDeltaAction) isStateAction() {} +func (*ChatToolCallReadyAction) isStateAction() {} +func (*ChatToolCallConfirmedAction) isStateAction() {} +func (*ChatToolCallCompleteAction) isStateAction() {} +func (*ChatToolCallResultConfirmedAction) isStateAction() {} +func (*ChatToolCallContentChangedAction) isStateAction() {} +func (*ChatTurnCompleteAction) isStateAction() {} +func (*ChatTurnCancelledAction) isStateAction() {} +func (*ChatErrorAction) isStateAction() {} func (*SessionTitleChangedAction) isStateAction() {} -func (*SessionUsageAction) isStateAction() {} -func (*SessionReasoningAction) isStateAction() {} +func (*ChatUsageAction) isStateAction() {} +func (*ChatReasoningAction) isStateAction() {} +func (*ChatPendingMessageSetAction) isStateAction() {} +func (*ChatPendingMessageRemovedAction) isStateAction() {} +func (*ChatQueuedMessagesReorderedAction) isStateAction() {} +func (*ChatInputRequestedAction) isStateAction() {} +func (*ChatInputAnswerChangedAction) isStateAction() {} +func (*ChatInputCompletedAction) isStateAction() {} +func (*ChatTruncatedAction) isStateAction() {} func (*SessionModelChangedAction) isStateAction() {} func (*SessionAgentChangedAction) isStateAction() {} func (*SessionIsReadChangedAction) isStateAction() {} @@ -1080,21 +1142,13 @@ func (*SessionChangesetsChangedAction) isStateAction() {} func (*SessionServerToolsChangedAction) isStateAction() {} func (*SessionActiveClientChangedAction) isStateAction() {} func (*SessionActiveClientToolsChangedAction) isStateAction() {} -func (*SessionPendingMessageSetAction) isStateAction() {} -func (*SessionPendingMessageRemovedAction) isStateAction() {} -func (*SessionQueuedMessagesReorderedAction) isStateAction() {} -func (*SessionInputRequestedAction) isStateAction() {} -func (*SessionInputAnswerChangedAction) isStateAction() {} -func (*SessionInputCompletedAction) isStateAction() {} func (*SessionCustomizationsChangedAction) isStateAction() {} func (*SessionCustomizationToggledAction) isStateAction() {} func (*SessionCustomizationUpdatedAction) isStateAction() {} func (*SessionCustomizationRemovedAction) isStateAction() {} func (*SessionMcpServerStateChangedAction) isStateAction() {} -func (*SessionTruncatedAction) isStateAction() {} func (*SessionConfigChangedAction) isStateAction() {} func (*SessionMetaChangedAction) isStateAction() {} -func (*SessionToolCallContentChangedAction) isStateAction() {} func (*ChangesetStatusChangedAction) isStateAction() {} func (*ChangesetFileSetAction) isStateAction() {} func (*ChangesetFileRemovedAction) isStateAction() {} @@ -1164,74 +1218,104 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value - case "session/turnStarted": - var value SessionTurnStartedAction + case "session/chatAdded": + var value SessionChatAddedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "session/chatRemoved": + var value SessionChatRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "session/chatUpdated": + var value SessionChatUpdatedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "session/defaultChatChanged": + var value SessionDefaultChatChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "chat/turnStarted": + var value ChatTurnStartedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/delta": - var value SessionDeltaAction + case "chat/delta": + var value ChatDeltaAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/responsePart": - var value SessionResponsePartAction + case "chat/responsePart": + var value ChatResponsePartAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/toolCallStart": - var value SessionToolCallStartAction + case "chat/toolCallStart": + var value ChatToolCallStartAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/toolCallDelta": - var value SessionToolCallDeltaAction + case "chat/toolCallDelta": + var value ChatToolCallDeltaAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/toolCallReady": - var value SessionToolCallReadyAction + case "chat/toolCallReady": + var value ChatToolCallReadyAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/toolCallConfirmed": - var value SessionToolCallConfirmedAction + case "chat/toolCallConfirmed": + var value ChatToolCallConfirmedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/toolCallComplete": - var value SessionToolCallCompleteAction + case "chat/toolCallComplete": + var value ChatToolCallCompleteAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/toolCallResultConfirmed": - var value SessionToolCallResultConfirmedAction + case "chat/toolCallResultConfirmed": + var value ChatToolCallResultConfirmedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/turnComplete": - var value SessionTurnCompleteAction + case "chat/toolCallContentChanged": + var value ChatToolCallContentChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/turnCancelled": - var value SessionTurnCancelledAction + case "chat/turnComplete": + var value ChatTurnCompleteAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/error": - var value SessionErrorAction + case "chat/turnCancelled": + var value ChatTurnCancelledAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "chat/error": + var value ChatErrorAction if err := json.Unmarshal(data, &value); err != nil { return err } @@ -1242,104 +1326,110 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value - case "session/usage": - var value SessionUsageAction + case "chat/usage": + var value ChatUsageAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/reasoning": - var value SessionReasoningAction + case "chat/reasoning": + var value ChatReasoningAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/modelChanged": - var value SessionModelChangedAction + case "chat/pendingMessageSet": + var value ChatPendingMessageSetAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/agentChanged": - var value SessionAgentChangedAction + case "chat/pendingMessageRemoved": + var value ChatPendingMessageRemovedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/isReadChanged": - var value SessionIsReadChangedAction + case "chat/queuedMessagesReordered": + var value ChatQueuedMessagesReorderedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/isArchivedChanged": - var value SessionIsArchivedChangedAction + case "chat/inputRequested": + var value ChatInputRequestedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/activityChanged": - var value SessionActivityChangedAction + case "chat/inputAnswerChanged": + var value ChatInputAnswerChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/changesetsChanged": - var value SessionChangesetsChangedAction + case "chat/inputCompleted": + var value ChatInputCompletedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/serverToolsChanged": - var value SessionServerToolsChangedAction + case "chat/truncated": + var value ChatTruncatedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/activeClientChanged": - var value SessionActiveClientChangedAction + case "session/modelChanged": + var value SessionModelChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/activeClientToolsChanged": - var value SessionActiveClientToolsChangedAction + case "session/agentChanged": + var value SessionAgentChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/pendingMessageSet": - var value SessionPendingMessageSetAction + case "session/isReadChanged": + var value SessionIsReadChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "session/isArchivedChanged": + var value SessionIsArchivedChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/pendingMessageRemoved": - var value SessionPendingMessageRemovedAction + case "session/activityChanged": + var value SessionActivityChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/queuedMessagesReordered": - var value SessionQueuedMessagesReorderedAction + case "session/changesetsChanged": + var value SessionChangesetsChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/inputRequested": - var value SessionInputRequestedAction + case "session/serverToolsChanged": + var value SessionServerToolsChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/inputAnswerChanged": - var value SessionInputAnswerChangedAction + case "session/activeClientChanged": + var value SessionActiveClientChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value - case "session/inputCompleted": - var value SessionInputCompletedAction + case "session/activeClientToolsChanged": + var value SessionActiveClientToolsChangedAction if err := json.Unmarshal(data, &value); err != nil { return err } @@ -1374,12 +1464,6 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value - case "session/truncated": - var value SessionTruncatedAction - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value case "session/configChanged": var value SessionConfigChangedAction if err := json.Unmarshal(data, &value); err != nil { @@ -1392,12 +1476,6 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value - case "session/toolCallContentChanged": - var value SessionToolCallContentChangedAction - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value case "changeset/statusChanged": var value ChangesetStatusChangedAction if err := json.Unmarshal(data, &value); err != nil { diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index a48150c5..9bf01aef 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -262,6 +262,36 @@ type DisposeSessionParams struct { Channel URI `json:"channel"` } +// Identifies a source chat and turn to fork from. +type ChatForkSource struct { + // URI of the existing chat to fork from + Chat URI `json:"chat"` + // Turn ID in the source chat; content up to and including this turn's response is copied + TurnId string `json:"turnId"` +} + +// Creates a new chat within a session. +type CreateChatParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Chat URI (client-chosen, e.g. `ahp-chat:/`). + Chat URI `json:"chat"` + // Optional initial message for the new chat. + InitialMessage *Message `json:"initialMessage,omitempty"` + // Optional per-chat model override. + Model *ModelSelection `json:"model,omitempty"` + // Optional per-chat agent override. + Agent *AgentSelection `json:"agent,omitempty"` + // Optional source chat and turn to fork from. + Source *ChatForkSource `json:"source,omitempty"` +} + +// Disposes a chat and cleans up server-side resources. +type DisposeChatParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` +} + // Returns a list of session summaries. Used to populate session lists and sidebars. // // The session list is **not** part of the state tree because it can be arbitrarily @@ -618,7 +648,7 @@ type CreateResourceWatchResult struct { Channel URI `json:"channel"` } -// Fetches historical turns for a session. Used for lazy loading of conversation +// Fetches historical turns for a chat. Used for lazy loading of conversation // history. type FetchTurnsParams struct { // Channel URI this command targets. diff --git a/clients/go/ahptypes/common.go b/clients/go/ahptypes/common.go index eb063deb..1d8f529f 100644 --- a/clients/go/ahptypes/common.go +++ b/clients/go/ahptypes/common.go @@ -69,6 +69,28 @@ type JSONObject = map[string]json.RawMessage // TypeScript `unknown` type). type AnyValue = json.RawMessage +// PartialChatSummary is the partial equivalent of ChatSummary — every field is optional for delta updates. +type PartialChatSummary struct { + // Chat URI. Ignored by session/chatUpdated reducers; chat identity never changes. + Resource *URI `json:"resource,omitempty"` + // Chat title + Title *string `json:"title,omitempty"` + // Current chat status (reuses SessionStatus shape) + Status *SessionStatus `json:"status,omitempty"` + // Human-readable description of what the chat is currently doing + Activity *string `json:"activity,omitempty"` + // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + ModifiedAt *string `json:"modifiedAt,omitempty"` + // Optional per-chat model override (defaults to the session's model) + Model *ModelSelection `json:"model,omitempty"` + // Optional per-chat agent override (defaults to the session's agent) + Agent *AgentSelection `json:"agent,omitempty"` + // How this chat came into existence + Origin *ChatOrigin `json:"origin,omitempty"` + // Optional per-chat working directory. + WorkingDirectory *URI `json:"workingDirectory,omitempty"` +} + // ─── StringOrMarkdown ──────────────────────────────────────────────────── // StringOrMarkdown is a wire value that may be either a plain JSON diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index f74c8b2f..19d04e14 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -182,7 +182,10 @@ type PartialSessionSummary struct { // Absent (`undefined`) means no custom agent is selected for this session // — the session uses the provider's default behavior. Agent *AgentSelection `json:"agent,omitempty"` - // The working directory URI for this session + // The default working directory URI for this session. Individual chats + // MAY override via {@link ChatSummary.workingDirectory | their own + // `workingDirectory`}; this field acts as the fallback for any chat that + // does not. WorkingDirectory *URI `json:"workingDirectory,omitempty"` // Aggregate summary of file changes associated with this session. Servers // may populate this to give clients a quick at-a-glance view of the diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index c7b72ec1..91d13128 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -24,16 +24,6 @@ const ( PolicyStateUnconfigured PolicyState = "unconfigured" ) -// Discriminant for pending message kinds. -type PendingMessageKind string - -const ( - // Injected into the current turn at a convenient point - PendingMessageKindSteering PendingMessageKind = "steering" - // Sent automatically as a new turn after the current turn finishes - PendingMessageKindQueued PendingMessageKind = "queued" -) - // Session initialization state. type SessionLifecycle string @@ -71,45 +61,63 @@ func (s SessionStatus) Has(other SessionStatus) bool { return s&other == other } // Or returns s combined with the flags in other. func (s SessionStatus) Or(other SessionStatus) SessionStatus { return s | other } +type ChatOriginKind string + +const ( + ChatOriginKindUser ChatOriginKind = "user" + ChatOriginKindFork ChatOriginKind = "fork" + ChatOriginKindTool ChatOriginKind = "tool" +) + +// Discriminant for pending message kinds. +type PendingMessageKind string + +const ( + // Injected into the current turn at a convenient point + PendingMessageKindSteering PendingMessageKind = "steering" + // Sent automatically as a new turn after the current turn finishes + PendingMessageKindQueued PendingMessageKind = "queued" +) + // Answer lifecycle state. -type SessionInputAnswerState string +type ChatInputAnswerState string const ( - SessionInputAnswerStateDraft SessionInputAnswerState = "draft" - SessionInputAnswerStateSubmitted SessionInputAnswerState = "submitted" - SessionInputAnswerStateSkipped SessionInputAnswerState = "skipped" + ChatInputAnswerStateDraft ChatInputAnswerState = "draft" + ChatInputAnswerStateSubmitted ChatInputAnswerState = "submitted" + ChatInputAnswerStateSkipped ChatInputAnswerState = "skipped" ) // Answer value kind. -type SessionInputAnswerValueKind string +type ChatInputAnswerValueKind string const ( - SessionInputAnswerValueKindText SessionInputAnswerValueKind = "text" - SessionInputAnswerValueKindNumber SessionInputAnswerValueKind = "number" - SessionInputAnswerValueKindBoolean SessionInputAnswerValueKind = "boolean" - SessionInputAnswerValueKindSelected SessionInputAnswerValueKind = "selected" - SessionInputAnswerValueKindSelectedMany SessionInputAnswerValueKind = "selected-many" + ChatInputAnswerValueKindText ChatInputAnswerValueKind = "text" + ChatInputAnswerValueKindNumber ChatInputAnswerValueKind = "number" + ChatInputAnswerValueKindBoolean ChatInputAnswerValueKind = "boolean" + ChatInputAnswerValueKindSelected ChatInputAnswerValueKind = "selected" + ChatInputAnswerValueKindSelectedMany ChatInputAnswerValueKind = "selected-many" ) // Question/input control kind. -type SessionInputQuestionKind string +type ChatInputQuestionKind string const ( - SessionInputQuestionKindText SessionInputQuestionKind = "text" - SessionInputQuestionKindNumber SessionInputQuestionKind = "number" - SessionInputQuestionKindInteger SessionInputQuestionKind = "integer" - SessionInputQuestionKindBoolean SessionInputQuestionKind = "boolean" - SessionInputQuestionKindSingleSelect SessionInputQuestionKind = "single-select" - SessionInputQuestionKindMultiSelect SessionInputQuestionKind = "multi-select" + ChatInputQuestionKindText ChatInputQuestionKind = "text" + ChatInputQuestionKindNumber ChatInputQuestionKind = "number" + ChatInputQuestionKindInteger ChatInputQuestionKind = "integer" + ChatInputQuestionKindBoolean ChatInputQuestionKind = "boolean" + ChatInputQuestionKindSingleSelect ChatInputQuestionKind = "single-select" + ChatInputQuestionKindMultiSelect ChatInputQuestionKind = "multi-select" ) // How a client completed an input request. -type SessionInputResponseKind string +type ChatInputResponseKind string const ( - SessionInputResponseKindAccept SessionInputResponseKind = "accept" - SessionInputResponseKindDecline SessionInputResponseKind = "decline" - SessionInputResponseKindCancel SessionInputResponseKind = "cancel" + ChatInputResponseKindAccept ChatInputResponseKind = "accept" + ChatInputResponseKindDecline ChatInputResponseKind = "decline" + ChatInputResponseKindCancel ChatInputResponseKind = "cancel" ) // How a turn ended. @@ -573,18 +581,6 @@ type ConfigSchema struct { Required []string `json:"required,omitempty"` } -// A message queued for future delivery to the agent. -// -// Steering messages are injected into the current turn mid-flight. -// Queued messages are automatically started as new turns after the -// current turn naturally finishes. -type PendingMessage struct { - // Unique identifier for this pending message - Id string `json:"id"` - // The message that will start the next turn - Message Message `json:"message"` -} - // Full state for a single session, loaded when a client subscribes to the session's URI. type SessionState struct { // Lightweight session metadata @@ -597,16 +593,13 @@ type SessionState struct { ServerTools []ToolDefinition `json:"serverTools,omitempty"` // The client currently providing tools and interactive capabilities to this session ActiveClient *SessionActiveClient `json:"activeClient,omitempty"` - // Completed turns - Turns []Turn `json:"turns"` - // Currently in-progress turn - ActiveTurn *ActiveTurn `json:"activeTurn,omitempty"` - // Message to inject into the current turn at a convenient point - SteeringMessage *PendingMessage `json:"steeringMessage,omitempty"` - // Messages to send automatically as new turns after the current turn finishes - QueuedMessages []PendingMessage `json:"queuedMessages,omitempty"` - // Requests for user input that are currently blocking or informing session progress - InputRequests []SessionInputRequest `json:"inputRequests,omitempty"` + // Catalog of chats in this session. + Chats []ChatSummary `json:"chats"` + // The chat that receives input when the user addresses the session without + // selecting a specific chat. This is a UI routing hint, not a hierarchy + // marker — chats remain equal peers at the protocol level. Hosts MAY change + // this over the session's lifetime. + DefaultChat *URI `json:"defaultChat,omitempty"` // Session configuration schema and current values Config *SessionConfigState `json:"config,omitempty"` // Top-level customizations active in this session. @@ -663,6 +656,39 @@ type SessionActiveClient struct { Customizations []ClientPluginCustomization `json:"customizations,omitempty"` } +// Lightweight catalog entry summarizing one session. Surfaced via +// {@link RootChannelCommands.listSessions | `root/listSessions`} and +// `root/sessionAdded`/`root/sessionSummaryChanged` notifications. +// +// **Aggregation across chats.** Once a session contains more than one chat, +// several `SessionSummary` fields are derived from the underlying +// {@link SessionState.chats | chat catalog}. Producers SHOULD follow these +// rules so clients that only consume the session summary (e.g. a session +// list) still see meaningful state: +// +// - `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` / +// `Error` — bits 0–4) from the +// {@link SessionState.defaultChat | default chat} when present, else from +// the most recently modified chat. **Promote** `InputNeeded` whenever any +// chat in the session needs input, and **promote** `Error` whenever any +// chat is in an error state — both override the default-chat bits. The +// orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped. +// - `activity`: mirror the activity string of the default chat, or of the +// chat currently driving the promoted status bits when a non-default chat +// wins (e.g. the chat that raised `InputNeeded`). +// - `modifiedAt`: the max of all chats' `modifiedAt`. +// - `model` / `agent`: the session-level selection. Per-chat overrides are +// surfaced on individual {@link ChatSummary} entries, not aggregated up. +// - `workingDirectory`: the session-level **default**. Individual chats MAY +// override via {@link ChatSummary.workingDirectory}; aggregating these up +// is meaningless and SHOULD NOT be attempted. +// - `changes`: optional roll-up across all chats. Producers MAY sum the +// per-chat changeset stats or report the most expensive chat's stats — +// whichever is cheaper for the host to compute. +// +// Sessions with a single chat trivially satisfy all of the above (the chat's +// values pass through unchanged). The rules only matter once a session +// carries multiple chats. type SessionSummary struct { // Session URI Resource URI `json:"resource"` @@ -687,7 +713,10 @@ type SessionSummary struct { // Absent (`undefined`) means no custom agent is selected for this session // — the session uses the provider's default behavior. Agent *AgentSelection `json:"agent,omitempty"` - // The working directory URI for this session + // The default working directory URI for this session. Individual chats + // MAY override via {@link ChatSummary.workingDirectory | their own + // `workingDirectory`}; this field acts as the fallback for any chat that + // does not. WorkingDirectory *URI `json:"workingDirectory,omitempty"` // Aggregate summary of file changes associated with this session. Servers // may populate this to give clients a quick at-a-glance view of the @@ -714,6 +743,96 @@ type ChangesSummary struct { Files *int64 `json:"files,omitempty"` } +// Full state for a single chat, loaded when a client subscribes to the chat's +// URI. +// +// The lightweight catalog representation of a chat is {@link ChatSummary}, +// carried in {@link SessionState.chats | `SessionState.chats`}. `ChatState` +// **denormalizes** every {@link ChatSummary} field directly onto itself so +// subscribers receive one flat object instead of having to merge a nested +// `summary` sub-object. Producers MUST keep the two representations +// consistent: any change to the inlined fields below SHOULD also be +// announced on the parent session via the matching +// {@link SessionChatUpdatedAction | `session/chatUpdated`} action. +type ChatState struct { + // Chat URI + Resource URI `json:"resource"` + // Chat title + Title string `json:"title"` + // Current chat status (reuses SessionStatus shape) + Status SessionStatus `json:"status"` + // Human-readable description of what the chat is currently doing + Activity *string `json:"activity,omitempty"` + // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + ModifiedAt string `json:"modifiedAt"` + // Optional per-chat model override (defaults to the session's model) + Model *ModelSelection `json:"model,omitempty"` + // Optional per-chat agent override (defaults to the session's agent) + Agent *AgentSelection `json:"agent,omitempty"` + // How this chat came into existence + Origin *ChatOrigin `json:"origin,omitempty"` + // Optional per-chat working directory. + // + // If absent, the chat inherits + // {@link SessionSummary.workingDirectory | the session's working directory}. + // Hosts MAY override this for individual chats — for example, to give a + // subordinate chat its own git worktree so multiple chats in a session can + // make independent edits that the orchestrator later merges back. + WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // Completed turns + Turns []Turn `json:"turns"` + // Currently in-progress turn + ActiveTurn *ActiveTurn `json:"activeTurn,omitempty"` + // Message to inject into the current turn at a convenient point + SteeringMessage *PendingMessage `json:"steeringMessage,omitempty"` + // Messages to send automatically as new turns after the current turn finishes + QueuedMessages []PendingMessage `json:"queuedMessages,omitempty"` + // Requests for user input that are currently blocking or informing chat progress + InputRequests []ChatInputRequest `json:"inputRequests,omitempty"` + // Additional provider-specific metadata for this chat. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + +// Lightweight catalog entry for a chat, carried in +// {@link SessionState.chats | `SessionState.chats`}. The full conversation +// lives in {@link ChatState}, which inlines (denormalizes) every field below. +type ChatSummary struct { + // Chat URI + Resource URI `json:"resource"` + // Chat title + Title string `json:"title"` + // Current chat status (reuses SessionStatus shape) + Status SessionStatus `json:"status"` + // Human-readable description of what the chat is currently doing + Activity *string `json:"activity,omitempty"` + // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + ModifiedAt string `json:"modifiedAt"` + // Optional per-chat model override (defaults to the session's model) + Model *ModelSelection `json:"model,omitempty"` + // Optional per-chat agent override (defaults to the session's agent) + Agent *AgentSelection `json:"agent,omitempty"` + // How this chat came into existence + Origin *ChatOrigin `json:"origin,omitempty"` + // Optional per-chat working directory. + // + // If absent, the chat inherits + // {@link SessionSummary.workingDirectory | the session's working directory}. + // See {@link ChatState.workingDirectory} for usage notes. + WorkingDirectory *URI `json:"workingDirectory,omitempty"` +} + +// A message queued for future delivery to the agent. +// +// Steering messages are injected into the current turn mid-flight. +// Queued messages are automatically started as new turns after the +// current turn naturally finishes. +type PendingMessage struct { + // Unique identifier for this pending message + Id string `json:"id"` + // The message that will start the next turn + Message Message `json:"message"` +} + // Server-owned project metadata for a session. type ProjectInfo struct { // Project URI @@ -835,7 +954,7 @@ type Message struct { } // A choice in a select-style question. -type SessionInputOption struct { +type ChatInputOption struct { // Stable option identifier; for MCP enum values this is the enum string Id string `json:"id"` // Display label @@ -847,51 +966,51 @@ type SessionInputOption struct { } // Value captured for one answer. -type SessionInputTextAnswerValue struct { - Kind SessionInputAnswerValueKind `json:"kind"` - Value string `json:"value"` +type ChatInputTextAnswerValue struct { + Kind ChatInputAnswerValueKind `json:"kind"` + Value string `json:"value"` } -type SessionInputNumberAnswerValue struct { - Kind SessionInputAnswerValueKind `json:"kind"` - Value float64 `json:"value"` +type ChatInputNumberAnswerValue struct { + Kind ChatInputAnswerValueKind `json:"kind"` + Value float64 `json:"value"` } -type SessionInputBooleanAnswerValue struct { - Kind SessionInputAnswerValueKind `json:"kind"` - Value bool `json:"value"` +type ChatInputBooleanAnswerValue struct { + Kind ChatInputAnswerValueKind `json:"kind"` + Value bool `json:"value"` } -type SessionInputSelectedAnswerValue struct { - Kind SessionInputAnswerValueKind `json:"kind"` - Value string `json:"value"` +type ChatInputSelectedAnswerValue struct { + Kind ChatInputAnswerValueKind `json:"kind"` + Value string `json:"value"` // Free-form text entered instead of selecting an option FreeformValues []string `json:"freeformValues,omitempty"` } -type SessionInputSelectedManyAnswerValue struct { - Kind SessionInputAnswerValueKind `json:"kind"` - Value []string `json:"value"` +type ChatInputSelectedManyAnswerValue struct { + Kind ChatInputAnswerValueKind `json:"kind"` + Value []string `json:"value"` // Free-form text entered in addition to selected options FreeformValues []string `json:"freeformValues,omitempty"` } -type SessionInputAnswered struct { +type ChatInputAnswered struct { // Answer state - State SessionInputAnswerState `json:"state"` + State ChatInputAnswerState `json:"state"` // Answer value - Value SessionInputAnswerValue `json:"value"` + Value ChatInputAnswerValue `json:"value"` } -type SessionInputSkipped struct { +type ChatInputSkipped struct { // Answer state - State SessionInputAnswerState `json:"state"` + State ChatInputAnswerState `json:"state"` // Free-form reason or value captured while skipping, if any FreeformValues []string `json:"freeformValues,omitempty"` } -// Text question within a session input request. -type SessionInputTextQuestion struct { +// Text question within a chat input request. +type ChatInputTextQuestion struct { // Stable question identifier used as the key in `answers` Id string `json:"id"` // Short display title @@ -899,8 +1018,8 @@ type SessionInputTextQuestion struct { // Prompt shown to the user Message string `json:"message"` // Whether the user must answer this question to accept the request - Required *bool `json:"required,omitempty"` - Kind SessionInputQuestionKind `json:"kind"` + Required *bool `json:"required,omitempty"` + Kind ChatInputQuestionKind `json:"kind"` // Format hint for text questions, such as `email`, `uri`, `date`, or `date-time` Format *string `json:"format,omitempty"` // Minimum string length @@ -911,8 +1030,8 @@ type SessionInputTextQuestion struct { DefaultValue *string `json:"defaultValue,omitempty"` } -// Numeric question within a session input request. -type SessionInputNumberQuestion struct { +// Numeric question within a chat input request. +type ChatInputNumberQuestion struct { // Stable question identifier used as the key in `answers` Id string `json:"id"` // Short display title @@ -920,8 +1039,8 @@ type SessionInputNumberQuestion struct { // Prompt shown to the user Message string `json:"message"` // Whether the user must answer this question to accept the request - Required *bool `json:"required,omitempty"` - Kind SessionInputQuestionKind `json:"kind"` + Required *bool `json:"required,omitempty"` + Kind ChatInputQuestionKind `json:"kind"` // Minimum value Min *float64 `json:"min,omitempty"` // Maximum value @@ -930,8 +1049,8 @@ type SessionInputNumberQuestion struct { DefaultValue *float64 `json:"defaultValue,omitempty"` } -// Boolean question within a session input request. -type SessionInputBooleanQuestion struct { +// Boolean question within a chat input request. +type ChatInputBooleanQuestion struct { // Stable question identifier used as the key in `answers` Id string `json:"id"` // Short display title @@ -939,14 +1058,14 @@ type SessionInputBooleanQuestion struct { // Prompt shown to the user Message string `json:"message"` // Whether the user must answer this question to accept the request - Required *bool `json:"required,omitempty"` - Kind SessionInputQuestionKind `json:"kind"` + Required *bool `json:"required,omitempty"` + Kind ChatInputQuestionKind `json:"kind"` // Default boolean value DefaultValue *bool `json:"defaultValue,omitempty"` } -// Single-select question within a session input request. -type SessionInputSingleSelectQuestion struct { +// Single-select question within a chat input request. +type ChatInputSingleSelectQuestion struct { // Stable question identifier used as the key in `answers` Id string `json:"id"` // Short display title @@ -954,16 +1073,16 @@ type SessionInputSingleSelectQuestion struct { // Prompt shown to the user Message string `json:"message"` // Whether the user must answer this question to accept the request - Required *bool `json:"required,omitempty"` - Kind SessionInputQuestionKind `json:"kind"` + Required *bool `json:"required,omitempty"` + Kind ChatInputQuestionKind `json:"kind"` // Options the user may select from - Options []SessionInputOption `json:"options"` + Options []ChatInputOption `json:"options"` // Whether the user may enter text instead of selecting an option AllowFreeformInput *bool `json:"allowFreeformInput,omitempty"` } -// Multi-select question within a session input request. -type SessionInputMultiSelectQuestion struct { +// Multi-select question within a chat input request. +type ChatInputMultiSelectQuestion struct { // Stable question identifier used as the key in `answers` Id string `json:"id"` // Short display title @@ -971,10 +1090,10 @@ type SessionInputMultiSelectQuestion struct { // Prompt shown to the user Message string `json:"message"` // Whether the user must answer this question to accept the request - Required *bool `json:"required,omitempty"` - Kind SessionInputQuestionKind `json:"kind"` + Required *bool `json:"required,omitempty"` + Kind ChatInputQuestionKind `json:"kind"` // Options the user may select from - Options []SessionInputOption `json:"options"` + Options []ChatInputOption `json:"options"` // Whether the user may enter text in addition to selecting options AllowFreeformInput *bool `json:"allowFreeformInput,omitempty"` // Minimum selected item count @@ -985,10 +1104,10 @@ type SessionInputMultiSelectQuestion struct { // A live request for user input. // -// The server creates or replaces requests with `session/inputRequested`. -// Clients sync drafts with `session/inputAnswerChanged` and complete requests -// with `session/inputCompleted`. -type SessionInputRequest struct { +// The server creates or replaces requests with `chat/inputRequested`. +// Clients sync drafts with `chat/inputAnswerChanged` and complete requests +// with `chat/inputCompleted`. +type ChatInputRequest struct { // Stable request identifier Id string `json:"id"` // Display message for the request as a whole @@ -996,9 +1115,9 @@ type SessionInputRequest struct { // URL the user should review or open, for URL-style elicitations Url *URI `json:"url,omitempty"` // Ordered questions to ask the user - Questions []SessionInputQuestion `json:"questions,omitempty"` + Questions []ChatInputQuestion `json:"questions,omitempty"` // Current draft or submitted answers, keyed by question ID - Answers map[string]SessionInputAnswer `json:"answers,omitempty"` + Answers map[string]ChatInputAnswer `json:"answers,omitempty"` } // A zero-based position within a textual document. @@ -1191,7 +1310,7 @@ type MessageAnnotationsAttachment struct { type MarkdownResponsePart struct { // Discriminant Kind ResponsePartKind `json:"kind"` - // Part identifier, used by `session/delta` to target this part for content appends + // Part identifier, used by `chat/delta` to target this part for content appends Id string `json:"id"` // Markdown content Content string `json:"content"` @@ -1235,7 +1354,7 @@ type ToolCallResponsePart struct { type ReasoningResponsePart struct { // Discriminant Kind ResponsePartKind `json:"kind"` - // Part identifier, used by `session/reasoning` to target this part for content appends + // Part identifier, used by `chat/reasoning` to target this part for content appends Id string `json:"id"` // Accumulated reasoning text Content string `json:"content"` @@ -2128,7 +2247,7 @@ type ToolCallClientContributor struct { // Absent for server-side tools. // // When set, the identified client is responsible for executing the tool and - // dispatching `session/toolCallComplete` with the result. + // dispatching `chat/toolCallComplete` with the result. ClientId string `json:"clientId"` } @@ -2272,7 +2391,7 @@ type ErrorInfo struct { // A point-in-time snapshot of a subscribed resource's state, returned by // `initialize`, `reconnect`, and `subscribe`. type Snapshot struct { - // The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`) + // The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`) Resource URI `json:"resource"` // The current state of the resource State SnapshotState `json:"state"` @@ -2847,67 +2966,67 @@ func (u TerminalContentPart) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } -// SessionInputQuestion is one question within a session input request. -type SessionInputQuestion struct { - Value isSessionInputQuestion +// ChatInputQuestion is one question within a chat input request. +type ChatInputQuestion struct { + Value isChatInputQuestion } -// isSessionInputQuestion is the marker interface implemented by every -// concrete variant of SessionInputQuestion. -type isSessionInputQuestion interface{ isSessionInputQuestion() } +// isChatInputQuestion is the marker interface implemented by every +// concrete variant of ChatInputQuestion. +type isChatInputQuestion interface{ isChatInputQuestion() } -func (*SessionInputTextQuestion) isSessionInputQuestion() {} -func (*SessionInputNumberQuestion) isSessionInputQuestion() {} -func (*SessionInputBooleanQuestion) isSessionInputQuestion() {} -func (*SessionInputSingleSelectQuestion) isSessionInputQuestion() {} -func (*SessionInputMultiSelectQuestion) isSessionInputQuestion() {} +func (*ChatInputTextQuestion) isChatInputQuestion() {} +func (*ChatInputNumberQuestion) isChatInputQuestion() {} +func (*ChatInputBooleanQuestion) isChatInputQuestion() {} +func (*ChatInputSingleSelectQuestion) isChatInputQuestion() {} +func (*ChatInputMultiSelectQuestion) isChatInputQuestion() {} -// SessionInputQuestionUnknown carries an unrecognized SessionInputQuestion 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 SessionInputQuestionUnknown struct { +// ChatInputQuestionUnknown carries an unrecognized ChatInputQuestion 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 ChatInputQuestionUnknown struct { Raw json.RawMessage } -func (*SessionInputQuestionUnknown) isSessionInputQuestion() {} +func (*ChatInputQuestionUnknown) isChatInputQuestion() {} // UnmarshalJSON decodes the variant indicated by the "kind" discriminator. -func (u *SessionInputQuestion) UnmarshalJSON(data []byte) error { +func (u *ChatInputQuestion) UnmarshalJSON(data []byte) error { disc, _, err := readDiscriminator(data, "kind") if err != nil { return err } switch disc { case "text": - var value SessionInputTextQuestion + var value ChatInputTextQuestion if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "number": - var value SessionInputNumberQuestion + var value ChatInputNumberQuestion if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "integer": - var value SessionInputNumberQuestion + var value ChatInputNumberQuestion if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "boolean": - var value SessionInputBooleanQuestion + var value ChatInputBooleanQuestion if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "single-select": - var value SessionInputSingleSelectQuestion + var value ChatInputSingleSelectQuestion if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "multi-select": - var value SessionInputMultiSelectQuestion + var value ChatInputMultiSelectQuestion if err := json.Unmarshal(data, &value); err != nil { return err } @@ -2915,14 +3034,14 @@ func (u *SessionInputQuestion) UnmarshalJSON(data []byte) error { default: raw := make(json.RawMessage, len(data)) copy(raw, data) - u.Value = &SessionInputQuestionUnknown{Raw: raw} + u.Value = &ChatInputQuestionUnknown{Raw: raw} } return nil } // MarshalJSON encodes the active variant back to JSON. -func (u SessionInputQuestion) MarshalJSON() ([]byte, error) { - if unk, ok := u.Value.(*SessionInputQuestionUnknown); ok { +func (u ChatInputQuestion) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*ChatInputQuestionUnknown); ok { if len(unk.Raw) == 0 { return []byte("null"), nil } @@ -2934,61 +3053,61 @@ func (u SessionInputQuestion) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } -// SessionInputAnswerValue is the value captured for one answer. -type SessionInputAnswerValue struct { - Value isSessionInputAnswerValue +// ChatInputAnswerValue is the value captured for one answer. +type ChatInputAnswerValue struct { + Value isChatInputAnswerValue } -// isSessionInputAnswerValue is the marker interface implemented by every -// concrete variant of SessionInputAnswerValue. -type isSessionInputAnswerValue interface{ isSessionInputAnswerValue() } +// isChatInputAnswerValue is the marker interface implemented by every +// concrete variant of ChatInputAnswerValue. +type isChatInputAnswerValue interface{ isChatInputAnswerValue() } -func (*SessionInputTextAnswerValue) isSessionInputAnswerValue() {} -func (*SessionInputNumberAnswerValue) isSessionInputAnswerValue() {} -func (*SessionInputBooleanAnswerValue) isSessionInputAnswerValue() {} -func (*SessionInputSelectedAnswerValue) isSessionInputAnswerValue() {} -func (*SessionInputSelectedManyAnswerValue) isSessionInputAnswerValue() {} +func (*ChatInputTextAnswerValue) isChatInputAnswerValue() {} +func (*ChatInputNumberAnswerValue) isChatInputAnswerValue() {} +func (*ChatInputBooleanAnswerValue) isChatInputAnswerValue() {} +func (*ChatInputSelectedAnswerValue) isChatInputAnswerValue() {} +func (*ChatInputSelectedManyAnswerValue) isChatInputAnswerValue() {} -// SessionInputAnswerValueUnknown carries an unrecognized SessionInputAnswerValue 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 SessionInputAnswerValueUnknown struct { +// ChatInputAnswerValueUnknown carries an unrecognized ChatInputAnswerValue 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 ChatInputAnswerValueUnknown struct { Raw json.RawMessage } -func (*SessionInputAnswerValueUnknown) isSessionInputAnswerValue() {} +func (*ChatInputAnswerValueUnknown) isChatInputAnswerValue() {} // UnmarshalJSON decodes the variant indicated by the "kind" discriminator. -func (u *SessionInputAnswerValue) UnmarshalJSON(data []byte) error { +func (u *ChatInputAnswerValue) UnmarshalJSON(data []byte) error { disc, _, err := readDiscriminator(data, "kind") if err != nil { return err } switch disc { case "text": - var value SessionInputTextAnswerValue + var value ChatInputTextAnswerValue if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "number": - var value SessionInputNumberAnswerValue + var value ChatInputNumberAnswerValue if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "boolean": - var value SessionInputBooleanAnswerValue + var value ChatInputBooleanAnswerValue if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "selected": - var value SessionInputSelectedAnswerValue + var value ChatInputSelectedAnswerValue if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "selected-many": - var value SessionInputSelectedManyAnswerValue + var value ChatInputSelectedManyAnswerValue if err := json.Unmarshal(data, &value); err != nil { return err } @@ -2996,14 +3115,14 @@ func (u *SessionInputAnswerValue) UnmarshalJSON(data []byte) error { default: raw := make(json.RawMessage, len(data)) copy(raw, data) - u.Value = &SessionInputAnswerValueUnknown{Raw: raw} + u.Value = &ChatInputAnswerValueUnknown{Raw: raw} } return nil } // MarshalJSON encodes the active variant back to JSON. -func (u SessionInputAnswerValue) MarshalJSON() ([]byte, error) { - if unk, ok := u.Value.(*SessionInputAnswerValueUnknown); ok { +func (u ChatInputAnswerValue) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*ChatInputAnswerValueUnknown); ok { if len(unk.Raw) == 0 { return []byte("null"), nil } @@ -3015,46 +3134,46 @@ func (u SessionInputAnswerValue) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } -// SessionInputAnswer is a draft, submitted, or skipped answer for one question. -type SessionInputAnswer struct { - Value isSessionInputAnswer +// ChatInputAnswer is a draft, submitted, or skipped answer for one question. +type ChatInputAnswer struct { + Value isChatInputAnswer } -// isSessionInputAnswer is the marker interface implemented by every -// concrete variant of SessionInputAnswer. -type isSessionInputAnswer interface{ isSessionInputAnswer() } +// isChatInputAnswer is the marker interface implemented by every +// concrete variant of ChatInputAnswer. +type isChatInputAnswer interface{ isChatInputAnswer() } -func (*SessionInputAnswered) isSessionInputAnswer() {} -func (*SessionInputSkipped) isSessionInputAnswer() {} +func (*ChatInputAnswered) isChatInputAnswer() {} +func (*ChatInputSkipped) isChatInputAnswer() {} -// SessionInputAnswerUnknown carries an unrecognized SessionInputAnswer 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 SessionInputAnswerUnknown struct { +// ChatInputAnswerUnknown carries an unrecognized ChatInputAnswer 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 ChatInputAnswerUnknown struct { Raw json.RawMessage } -func (*SessionInputAnswerUnknown) isSessionInputAnswer() {} +func (*ChatInputAnswerUnknown) isChatInputAnswer() {} // UnmarshalJSON decodes the variant indicated by the "state" discriminator. -func (u *SessionInputAnswer) UnmarshalJSON(data []byte) error { +func (u *ChatInputAnswer) UnmarshalJSON(data []byte) error { disc, _, err := readDiscriminator(data, "state") if err != nil { return err } switch disc { case "draft": - var value SessionInputAnswered + var value ChatInputAnswered if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "submitted": - var value SessionInputAnswered + var value ChatInputAnswered if err := json.Unmarshal(data, &value); err != nil { return err } u.Value = &value case "skipped": - var value SessionInputSkipped + var value ChatInputSkipped if err := json.Unmarshal(data, &value); err != nil { return err } @@ -3062,14 +3181,14 @@ func (u *SessionInputAnswer) UnmarshalJSON(data []byte) error { default: raw := make(json.RawMessage, len(data)) copy(raw, data) - u.Value = &SessionInputAnswerUnknown{Raw: raw} + u.Value = &ChatInputAnswerUnknown{Raw: raw} } return nil } // MarshalJSON encodes the active variant back to JSON. -func (u SessionInputAnswer) MarshalJSON() ([]byte, error) { - if unk, ok := u.Value.(*SessionInputAnswerUnknown); ok { +func (u ChatInputAnswer) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*ChatInputAnswerUnknown); ok { if len(unk.Raw) == 0 { return []byte("null"), nil } @@ -3613,14 +3732,96 @@ func (u ToolCallContributor) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// ChatOrigin describes how a chat came into existence. +type ChatOrigin struct { + Value isChatOrigin +} + +// isChatOrigin is the marker interface for chat origin variants. +type isChatOrigin interface{ isChatOrigin() } + +type ChatUserOrigin struct { + Kind ChatOriginKind `json:"kind"` +} + +func (*ChatUserOrigin) isChatOrigin() {} + +type ChatForkOrigin struct { + Kind ChatOriginKind `json:"kind"` + Chat URI `json:"chat"` + TurnId string `json:"turnId"` +} + +func (*ChatForkOrigin) isChatOrigin() {} + +type ChatToolOrigin struct { + Kind ChatOriginKind `json:"kind"` + Chat URI `json:"chat"` + ToolCallId string `json:"toolCallId"` +} + +func (*ChatToolOrigin) isChatOrigin() {} + +type ChatOriginUnknown struct { + Raw json.RawMessage +} + +func (*ChatOriginUnknown) isChatOrigin() {} + +func (o *ChatOrigin) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + switch disc { + case "user": + var v ChatUserOrigin + if err := json.Unmarshal(data, &v); err != nil { + return err + } + o.Value = &v + case "fork": + var v ChatForkOrigin + if err := json.Unmarshal(data, &v); err != nil { + return err + } + o.Value = &v + case "tool": + var v ChatToolOrigin + if err := json.Unmarshal(data, &v); err != nil { + return err + } + o.Value = &v + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + o.Value = &ChatOriginUnknown{Raw: raw} + } + return nil +} + +func (o ChatOrigin) MarshalJSON() ([]byte, error) { + if unk, ok := o.Value.(*ChatOriginUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if o.Value == nil { + return []byte("null"), nil + } + return json.Marshal(o.Value) +} + // SnapshotState is the state payload of a snapshot — root, session, -// terminal, changeset, resource-watch, or annotations state. The active +// chat, terminal, changeset, resource-watch, or annotations state. The active // variant is chosen by which pointer field is non-nil; UnmarshalJSON probes // for required fields in the canonical order -// (session → terminal → changeset → resourceWatch → annotations → root). +// (session → chat → terminal → changeset → resourceWatch → annotations → root). type SnapshotState struct { Root *RootState `json:"-"` Session *SessionState `json:"-"` + Chat *ChatState `json:"-"` Terminal *TerminalState `json:"-"` Changeset *ChangesetState `json:"-"` ResourceWatch *ResourceWatchState `json:"-"` @@ -3632,6 +3833,8 @@ func (s SnapshotState) MarshalJSON() ([]byte, error) { switch { case s.Session != nil: return json.Marshal(s.Session) + case s.Chat != nil: + return json.Marshal(s.Chat) case s.Terminal != nil: return json.Marshal(s.Terminal) case s.Changeset != nil: @@ -3662,6 +3865,12 @@ func (s *SnapshotState) UnmarshalJSON(data []byte) error { return err } s.Session = &v + case containsAll(probe, "summary", "turns"): + var v ChatState + if err := json.Unmarshal(data, &v); err != nil { + return err + } + s.Chat = &v case containsAll(probe, "content"): var v TerminalState if err := json.Unmarshal(data, &v); err != nil { diff --git a/clients/go/examples/reducers_demo/main.go b/clients/go/examples/reducers_demo/main.go index 9cfef5e8..0bea7ef7 100644 --- a/clients/go/examples/reducers_demo/main.go +++ b/clients/go/examples/reducers_demo/main.go @@ -1,5 +1,5 @@ -// Command reducers_demo applies a handful of session actions to an -// empty SessionState to illustrate the public reducer API. +// Command reducers_demo applies a handful of chat actions to an +// empty ChatState to illustrate the public reducer API. package main import ( @@ -11,25 +11,21 @@ import ( ) func main() { - state := ahptypes.SessionState{ - Summary: ahptypes.SessionSummary{ - Resource: "ahp-session:/demo", - Provider: "demo", - Title: "Demo", - Status: ahptypes.SessionStatusIdle, - CreatedAt: 1, - }, - Lifecycle: ahptypes.SessionLifecycleReady, + state := ahptypes.ChatState{ + Resource: "ahp-chat:/demo", + Title: "Demo", + Status: ahptypes.SessionStatusIdle, + ModifiedAt: "1970-01-01T00:00:00.001Z", } actions := []ahptypes.StateAction{ - {Value: &ahptypes.SessionTurnStartedAction{ - Type: ahptypes.ActionTypeSessionTurnStarted, + {Value: &ahptypes.ChatTurnStartedAction{ + Type: ahptypes.ActionTypeChatTurnStarted, TurnId: "t1", Message: ahptypes.Message{Text: "Hello!"}, }}, - {Value: &ahptypes.SessionResponsePartAction{ - Type: ahptypes.ActionTypeSessionResponsePart, + {Value: &ahptypes.ChatResponsePartAction{ + Type: ahptypes.ActionTypeChatResponsePart, TurnId: "t1", Part: ahptypes.ResponsePart{Value: &ahptypes.MarkdownResponsePart{ Kind: ahptypes.ResponsePartKindMarkdown, @@ -37,20 +33,20 @@ func main() { Content: "Hi ", }}, }}, - {Value: &ahptypes.SessionDeltaAction{ - Type: ahptypes.ActionTypeSessionDelta, + {Value: &ahptypes.ChatDeltaAction{ + Type: ahptypes.ActionTypeChatDelta, TurnId: "t1", PartId: "p1", Content: "there!", }}, - {Value: &ahptypes.SessionTurnCompleteAction{ - Type: ahptypes.ActionTypeSessionTurnComplete, + {Value: &ahptypes.ChatTurnCompleteAction{ + Type: ahptypes.ActionTypeChatTurnComplete, TurnId: "t1", }}, } for _, a := range actions { - outcome := ahp.ApplyActionToSession(&state, a) + outcome := ahp.ApplyActionToChat(&state, a) fmt.Printf("applied %T → %v\n", a.Value, outcomeName(outcome)) } diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 25586707..455db734 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -34,12 +34,22 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump resending its entries. Handled by the annotations reducer (no-op on unknown id). -### Added - +- `ahp-chat:` channel for per-chat conversation state; `SessionState.chats[]` catalog; `SessionState.defaultChat?` input-routing hint; `ChatOrigin` provenance union; `createChat` command. +- `ChatSummary.workingDirectory` — optional per-chat working directory. Falls back to the session's `workingDirectory` when absent. +- Three discrete chat-catalog actions on the session channel — `SessionChatAddedAction` (upsert by `summary.resource`), `SessionChatRemovedAction`, and `SessionChatUpdatedAction` (partial-update payload). - `RootState` now exposes an optional `_meta` property bag (`meta: Map?`) for implementation-defined agent-host metadata, such as a well-known `hostBuild` key carrying the host's build version/commit/date. +### Changed + +- `ChatState` is now flat — the previous embedded `summary` has been replaced with inlined `resource` / `title` / `status` / `activity` / `modifiedAt` / `model` / `agent` / `origin` / `workingDirectory` properties. `ChatSummary` remains as the standalone catalog entry on `SessionState.chats`. +- `ChatSummary.modifiedAt` and `ChatState.modifiedAt` are now ISO 8601 `String` values instead of `Long` milliseconds. + +### Removed + +- `SessionChatsChangedAction` (replaced by the three discrete chat-catalog actions above). + ## [0.3.0] — 2026-06-05 Implements AHP 0.3.0. @@ -84,11 +94,13 @@ Implements AHP 0.3.0. ### Changed +- `fetchTurns` and `completions` now target an `ahp-chat:` channel; `PROTOCOL_VERSION` bumped to `0.4.0`. - Renamed the `ChangesetSummary` type to `Changeset`. The on-the-wire shape is unchanged. - Moved the `changesets` catalogue from `SessionSummary` to `SessionState`. The `session/changesetsChanged` action now updates `state.changesets` directly instead of `state.summary.changesets`. ### Removed +- `SessionState.turns`, `SessionState.activeTurn`, `SessionState.steeringMessage`, `SessionState.queuedMessages`, `SessionState.inputRequests` (moved to `ChatState`). - Removed the `additions`, `deletions`, and `files` fields from `ChangesetSummary`. Aggregate counts now live on `SessionSummary.changes`; per-changeset views derive their own totals from `ChangesetState.files`. ### 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 c7a3b74f..a445fa22 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -8,143 +8,8 @@ package com.microsoft.agenthostprotocol -import com.microsoft.agenthostprotocol.generated.AgentSelection -import com.microsoft.agenthostprotocol.generated.ChangesetFile -import com.microsoft.agenthostprotocol.generated.ChangesetState -import com.microsoft.agenthostprotocol.generated.ChangesetStatus -import com.microsoft.agenthostprotocol.generated.ChangesetOperationStatus -import com.microsoft.agenthostprotocol.generated.ChildCustomization -import com.microsoft.agenthostprotocol.generated.ChildCustomizationAgent -import com.microsoft.agenthostprotocol.generated.ChildCustomizationHook -import com.microsoft.agenthostprotocol.generated.ChildCustomizationMcpServer -import com.microsoft.agenthostprotocol.generated.ChildCustomizationPrompt -import com.microsoft.agenthostprotocol.generated.ChildCustomizationRule -import com.microsoft.agenthostprotocol.generated.ChildCustomizationSkill -import com.microsoft.agenthostprotocol.generated.ChildCustomizationUnknown -import com.microsoft.agenthostprotocol.generated.AnnotationEntry -import com.microsoft.agenthostprotocol.generated.Annotation -import com.microsoft.agenthostprotocol.generated.AnnotationsState -import com.microsoft.agenthostprotocol.generated.ConfirmationOption -import com.microsoft.agenthostprotocol.generated.Customization -import com.microsoft.agenthostprotocol.generated.CustomizationDirectory -import com.microsoft.agenthostprotocol.generated.CustomizationMcpServer -import com.microsoft.agenthostprotocol.generated.CustomizationPlugin -import com.microsoft.agenthostprotocol.generated.CustomizationUnknown -import com.microsoft.agenthostprotocol.generated.ErrorInfo -import com.microsoft.agenthostprotocol.generated.MarkdownResponsePart -import com.microsoft.agenthostprotocol.generated.PendingMessage -import com.microsoft.agenthostprotocol.generated.PendingMessageKind -import com.microsoft.agenthostprotocol.generated.ReasoningResponsePart -import com.microsoft.agenthostprotocol.generated.ResponsePart -import com.microsoft.agenthostprotocol.generated.ResponsePartKind -import com.microsoft.agenthostprotocol.generated.ResponsePartMarkdown -import com.microsoft.agenthostprotocol.generated.ResponsePartReasoning -import com.microsoft.agenthostprotocol.generated.ResponsePartToolCall -import com.microsoft.agenthostprotocol.generated.RootState -import com.microsoft.agenthostprotocol.generated.SessionActiveClient -import com.microsoft.agenthostprotocol.generated.ResourceWatchState -import com.microsoft.agenthostprotocol.generated.SessionInputRequest -import com.microsoft.agenthostprotocol.generated.SessionLifecycle -import com.microsoft.agenthostprotocol.generated.SessionState -import com.microsoft.agenthostprotocol.generated.SessionStatus -import com.microsoft.agenthostprotocol.generated.SessionSummary -import com.microsoft.agenthostprotocol.generated.StateAction -import com.microsoft.agenthostprotocol.generated.StateActionChangesetCleared -import com.microsoft.agenthostprotocol.generated.StateActionChangesetFileRemoved -import com.microsoft.agenthostprotocol.generated.StateActionChangesetFileSet -import com.microsoft.agenthostprotocol.generated.StateActionChangesetOperationsChanged -import com.microsoft.agenthostprotocol.generated.StateActionChangesetOperationStatusChanged -import com.microsoft.agenthostprotocol.generated.StateActionChangesetStatusChanged -import com.microsoft.agenthostprotocol.generated.StateActionAnnotationsEntryRemoved -import com.microsoft.agenthostprotocol.generated.StateActionAnnotationsEntrySet -import com.microsoft.agenthostprotocol.generated.StateActionAnnotationsRemoved -import com.microsoft.agenthostprotocol.generated.StateActionAnnotationsSet -import com.microsoft.agenthostprotocol.generated.StateActionAnnotationsUpdated -import com.microsoft.agenthostprotocol.generated.StateActionRootActiveSessionsChanged -import com.microsoft.agenthostprotocol.generated.StateActionRootAgentsChanged -import com.microsoft.agenthostprotocol.generated.StateActionRootConfigChanged -import com.microsoft.agenthostprotocol.generated.StateActionRootTerminalsChanged -import com.microsoft.agenthostprotocol.generated.StateActionResourceWatchChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionActiveClientChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionActiveClientToolsChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionActivityChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionAgentChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionChangesetsChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionConfigChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionCreationFailed -import com.microsoft.agenthostprotocol.generated.StateActionSessionCustomizationRemoved -import com.microsoft.agenthostprotocol.generated.StateActionSessionCustomizationToggled -import com.microsoft.agenthostprotocol.generated.StateActionSessionCustomizationUpdated -import com.microsoft.agenthostprotocol.generated.StateActionSessionCustomizationsChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionDelta -import com.microsoft.agenthostprotocol.generated.StateActionSessionError -import com.microsoft.agenthostprotocol.generated.StateActionSessionInputAnswerChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionInputCompleted -import com.microsoft.agenthostprotocol.generated.StateActionSessionInputRequested -import com.microsoft.agenthostprotocol.generated.StateActionSessionIsArchivedChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionIsReadChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionMcpServerStateChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionMetaChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionModelChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionPendingMessageRemoved -import com.microsoft.agenthostprotocol.generated.StateActionSessionPendingMessageSet -import com.microsoft.agenthostprotocol.generated.StateActionSessionQueuedMessagesReordered -import com.microsoft.agenthostprotocol.generated.StateActionSessionReady -import com.microsoft.agenthostprotocol.generated.StateActionSessionReasoning -import com.microsoft.agenthostprotocol.generated.StateActionSessionResponsePart -import com.microsoft.agenthostprotocol.generated.StateActionSessionServerToolsChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionTitleChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionToolCallComplete -import com.microsoft.agenthostprotocol.generated.StateActionSessionToolCallConfirmed -import com.microsoft.agenthostprotocol.generated.StateActionSessionToolCallContentChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionToolCallDelta -import com.microsoft.agenthostprotocol.generated.StateActionSessionToolCallReady -import com.microsoft.agenthostprotocol.generated.StateActionSessionToolCallResultConfirmed -import com.microsoft.agenthostprotocol.generated.StateActionSessionToolCallStart -import com.microsoft.agenthostprotocol.generated.StateActionSessionTruncated -import com.microsoft.agenthostprotocol.generated.StateActionSessionTurnCancelled -import com.microsoft.agenthostprotocol.generated.StateActionSessionTurnComplete -import com.microsoft.agenthostprotocol.generated.StateActionSessionTurnStarted -import com.microsoft.agenthostprotocol.generated.StateActionSessionUsage -import com.microsoft.agenthostprotocol.generated.StateActionTerminalClaimed -import com.microsoft.agenthostprotocol.generated.StateActionTerminalCleared -import com.microsoft.agenthostprotocol.generated.StateActionTerminalCommandDetectionAvailable -import com.microsoft.agenthostprotocol.generated.StateActionTerminalCommandExecuted -import com.microsoft.agenthostprotocol.generated.StateActionTerminalCommandFinished -import com.microsoft.agenthostprotocol.generated.StateActionTerminalCwdChanged -import com.microsoft.agenthostprotocol.generated.StateActionTerminalData -import com.microsoft.agenthostprotocol.generated.StateActionTerminalExited -import com.microsoft.agenthostprotocol.generated.StateActionTerminalInput -import com.microsoft.agenthostprotocol.generated.StateActionTerminalResized -import com.microsoft.agenthostprotocol.generated.StateActionTerminalTitleChanged -import com.microsoft.agenthostprotocol.generated.TerminalCommandPart -import com.microsoft.agenthostprotocol.generated.TerminalContentPart -import com.microsoft.agenthostprotocol.generated.TerminalContentPartCommand -import com.microsoft.agenthostprotocol.generated.TerminalContentPartUnclassified -import com.microsoft.agenthostprotocol.generated.TerminalState -import com.microsoft.agenthostprotocol.generated.TerminalUnclassifiedPart -import com.microsoft.agenthostprotocol.generated.ToolCallCancellationReason -import com.microsoft.agenthostprotocol.generated.ToolCallCancelledState -import com.microsoft.agenthostprotocol.generated.ToolCallCompletedState -import com.microsoft.agenthostprotocol.generated.ToolCallConfirmationReason -import com.microsoft.agenthostprotocol.generated.ToolCallContributor -import com.microsoft.agenthostprotocol.generated.ToolCallPendingConfirmationState -import com.microsoft.agenthostprotocol.generated.ToolCallPendingResultConfirmationState -import com.microsoft.agenthostprotocol.generated.ToolCallResponsePart -import com.microsoft.agenthostprotocol.generated.ToolCallRunningState -import com.microsoft.agenthostprotocol.generated.ToolCallState -import com.microsoft.agenthostprotocol.generated.ToolCallStateCancelled -import com.microsoft.agenthostprotocol.generated.ToolCallStateCompleted -import com.microsoft.agenthostprotocol.generated.ToolCallStatePendingConfirmation -import com.microsoft.agenthostprotocol.generated.ToolCallStatePendingResultConfirmation -import com.microsoft.agenthostprotocol.generated.ToolCallStateRunning -import com.microsoft.agenthostprotocol.generated.ToolCallStateStreaming -import com.microsoft.agenthostprotocol.generated.ToolCallStateUnknown -import com.microsoft.agenthostprotocol.generated.ToolCallStatus -import com.microsoft.agenthostprotocol.generated.ToolCallStreamingState -import com.microsoft.agenthostprotocol.generated.Turn -import com.microsoft.agenthostprotocol.generated.ActiveTurn -import com.microsoft.agenthostprotocol.generated.TurnState +import com.microsoft.agenthostprotocol.generated.* +import java.time.Instant import kotlinx.serialization.json.JsonElement // ─── Reducer Interface ────────────────────────────────────────────────────── @@ -175,6 +40,12 @@ public object SessionReducer : Reducer { sessionReducer(state, action) } +/** Pure chat reducer as a [Reducer] instance. Delegates to [chatReducer]. */ +public object ChatReducer : Reducer { + override fun reduce(state: ChatState, action: StateAction): ChatState = + chatReducer(state, action) +} + /** Pure terminal reducer as a [Reducer] instance. Delegates to [terminalReducer]. */ public object TerminalReducer : Reducer { override fun reduce(state: TerminalState, action: StateAction): TerminalState = @@ -210,6 +81,7 @@ public object ResourceWatchReducer : Reducer { public var currentTimestampProvider: () -> Long = { System.currentTimeMillis() } private fun now(): Long = currentTimestampProvider() +private fun nowIsoString(): String = Instant.ofEpochMilli(currentTimestampProvider()).toString() // ─── Status Bitset Helpers ────────────────────────────────────────────────── @@ -225,7 +97,7 @@ private fun withStatusFlag(status: SessionStatus, flag: SessionStatus, set: Bool } /** Derives the summary status from live session work, preserving orthogonal flags. */ -private fun summaryStatus(state: SessionState, terminalStatus: SessionStatus? = null): SessionStatus { +private fun chatSummaryStatus(state: ChatState, terminalStatus: SessionStatus? = null): SessionStatus { val activity: SessionStatus = when { terminalStatus != null -> terminalStatus (state.inputRequests?.size ?: 0) > 0 || hasPendingToolCallConfirmation(state) -> @@ -233,25 +105,25 @@ private fun summaryStatus(state: SessionState, terminalStatus: SessionStatus? = state.activeTurn != null -> SessionStatus.IN_PROGRESS else -> SessionStatus.IDLE } - val preserved = state.summary.status.rawValue and STATUS_ACTIVITY_MASK.inv() + val preserved = state.status.rawValue and STATUS_ACTIVITY_MASK.inv() return SessionStatus(preserved or activity.rawValue) } /** - * Returns a state with `summary.status` recomputed. Use after reducers that - * change data feeding into [summaryStatus] (e.g. tool call lifecycle + * Returns a state with chat [ChatState.status] recomputed. Use after reducers that + * change data feeding into [chatSummaryStatus] (e.g. tool call lifecycle * transitions that may enter or leave a pending-confirmation state). */ -private fun refreshSummaryStatus(state: SessionState): SessionState { - val status = summaryStatus(state) - if (status.rawValue == state.summary.status.rawValue) { +private fun refreshChatSummaryStatus(state: ChatState): ChatState { + val status = chatSummaryStatus(state) + if (status.rawValue == state.status.rawValue) { return state } - return state.copy(summary = state.summary.copy(status = status)) + return state.copy(status = status) } /** Returns `true` if the active turn has any tool call awaiting user confirmation. */ -private fun hasPendingToolCallConfirmation(state: SessionState): Boolean { +private fun hasPendingToolCallConfirmation(state: ChatState): Boolean { val active = state.activeTurn ?: return false return active.responseParts.any { part -> part is ResponsePartToolCall && @@ -355,11 +227,11 @@ private fun childCustomizationId(c: ChildCustomization): String? = when (c) { * active turn or tool call doesn't match. */ private fun updateToolCallInParts( - state: SessionState, + state: ChatState, turnId: String, toolCallId: String, updater: (ToolCallState) -> ToolCallState, -): SessionState { +): ChatState { val activeTurn = state.activeTurn ?: return state if (activeTurn.id != turnId) return state @@ -387,11 +259,11 @@ private fun updateToolCallInParts( * matches on `toolCall.toolCallId`. */ private fun updateResponsePart( - state: SessionState, + state: ChatState, turnId: String, partId: String, updater: (ResponsePart) -> ResponsePart, -): SessionState { +): ChatState { val activeTurn = state.activeTurn ?: return state if (activeTurn.id != turnId) return state @@ -424,12 +296,12 @@ private fun updateResponsePart( * stripped from those tool call parts in the process. */ private fun endTurn( - state: SessionState, + state: ChatState, turnId: String, turnState: TurnState, terminalStatus: SessionStatus? = null, error: ErrorInfo? = null, -): SessionState { +): ChatState { val active = state.activeTurn ?: return state if (active.id != turnId) return state @@ -492,17 +364,15 @@ private fun endTurn( turns = state.turns + turn, activeTurn = null, inputRequests = null, - summary = state.summary.copy(modifiedAt = now()), - ) - return withoutTurn.copy( - summary = withoutTurn.summary.copy(status = summaryStatus(withoutTurn, terminalStatus)), + modifiedAt = nowIsoString(), ) + return withoutTurn.copy(status = chatSummaryStatus(withoutTurn, terminalStatus)) } -private fun upsertInputRequest(state: SessionState, request: SessionInputRequest): SessionState { +private fun upsertInputRequest(state: ChatState, request: ChatInputRequest): ChatState { val existing = state.inputRequests ?: emptyList() val idx = existing.indexOfFirst { it.id == request.id } - val updated: List = if (idx >= 0) { + val updated: List = if (idx >= 0) { val priorAnswers = existing[idx].answers existing.toMutableList().also { it[idx] = request.copy(answers = request.answers ?: priorAnswers) } } else { @@ -510,10 +380,8 @@ private fun upsertInputRequest(state: SessionState, request: SessionInputRequest } val next = state.copy(inputRequests = updated) return next.copy( - summary = next.summary.copy( - status = withStatusFlag(summaryStatus(next), SessionStatus.IS_READ, false), - modifiedAt = now(), - ), + status = withStatusFlag(chatSummaryStatus(next), SessionStatus.IS_READ, false), + modifiedAt = nowIsoString(), ) } @@ -559,25 +427,218 @@ public fun rootReducer(state: RootState, action: StateAction): RootState = when * no-ops that return [state] unchanged. */ public fun sessionReducer(state: SessionState, action: StateAction): SessionState = when (action) { - - // ── Lifecycle ────────────────────────────────────────────────────────── - - is StateActionSessionReady -> { - // Lifecycle-only transition (Creating → Ready). Must not touch - // `summary.status`: for provisional sessions the first turn can - // start before materialisation completes, so an `activeTurn` may - // already be set. The TS reference impl notes this in detail. - state.copy(lifecycle = SessionLifecycle.READY) - } + is StateActionSessionReady -> state.copy(lifecycle = SessionLifecycle.READY) is StateActionSessionCreationFailed -> state.copy( lifecycle = SessionLifecycle.CREATION_FAILED, creationError = action.value.error, ) + is StateActionSessionChatAdded -> { + val summary = action.value.summary + val idx = state.chats.indexOfFirst { it.resource == summary.resource } + if (idx < 0) { + state.copy(chats = state.chats + summary) + } else { + val updated = state.chats.toMutableList() + updated[idx] = summary + state.copy(chats = updated) + } + } + + is StateActionSessionChatRemoved -> { + val chat = action.value.chat + val idx = state.chats.indexOfFirst { it.resource == chat } + if (idx < 0) { + state + } else { + val updated = state.chats.toMutableList() + updated.removeAt(idx) + state.copy( + chats = updated, + defaultChat = if (state.defaultChat == chat) null else state.defaultChat, + ) + } + } + + is StateActionSessionChatUpdated -> { + val a = action.value + val idx = state.chats.indexOfFirst { it.resource == a.chat } + if (idx < 0) { + state + } else { + val prior = state.chats[idx] + val c = a.changes + val updatedSummary = prior.copy( + title = c.title ?: prior.title, + status = c.status ?: prior.status, + activity = c.activity ?: prior.activity, + modifiedAt = c.modifiedAt ?: prior.modifiedAt, + model = c.model ?: prior.model, + agent = c.agent ?: prior.agent, + origin = c.origin ?: prior.origin, + workingDirectory = c.workingDirectory ?: prior.workingDirectory, + ) + val updated = state.chats.toMutableList() + updated[idx] = updatedSummary + state.copy(chats = updated) + } + } + + is StateActionSessionDefaultChatChanged -> state.copy(defaultChat = action.value.defaultChat) + + is StateActionSessionTitleChanged -> state.copy( + summary = state.summary.copy(title = action.value.title, modifiedAt = now()), + ) + + is StateActionSessionModelChanged -> state.copy( + summary = state.summary.copy(model = action.value.model, modifiedAt = now()), + ) + + is StateActionSessionAgentChanged -> state.copy( + summary = state.summary.copy(agent = action.value.agent, modifiedAt = now()), + ) + + is StateActionSessionIsReadChanged -> state.copy( + summary = state.summary.copy( + status = withStatusFlag(state.summary.status, SessionStatus.IS_READ, action.value.isRead), + ), + ) + + is StateActionSessionIsArchivedChanged -> state.copy( + summary = state.summary.copy( + status = withStatusFlag(state.summary.status, SessionStatus.IS_ARCHIVED, action.value.isArchived), + ), + ) + + is StateActionSessionActivityChanged -> state.copy( + summary = state.summary.copy(activity = action.value.activity), + ) + + is StateActionSessionChangesetsChanged -> state.copy(changesets = action.value.changesets) + + is StateActionSessionConfigChanged -> { + val a = action.value + val config = state.config + if (config == null) state else { + val newValues = if (a.replace == true) a.config else config.values + a.config + state.copy(config = config.copy(values = newValues), summary = state.summary.copy(modifiedAt = now())) + } + } + + is StateActionSessionMetaChanged -> state.copy(meta = action.value.meta) + + is StateActionSessionServerToolsChanged -> state.copy(serverTools = action.value.tools) + + is StateActionSessionActiveClientChanged -> state.copy(activeClient = action.value.activeClient) + + is StateActionSessionActiveClientToolsChanged -> { + val client = state.activeClient + if (client == null) state else state.copy(activeClient = client.copy(tools = action.value.tools)) + } + + is StateActionSessionCustomizationsChanged -> state.copy(customizations = action.value.customizations) + + is StateActionSessionCustomizationToggled -> { + val a = action.value + val list = state.customizations + if (list == null) state else { + val idx = list.indexOfFirst { customizationId(it) == a.id } + if (idx < 0) state else { + val updated = list.toMutableList() + updated[idx] = withCustomizationEnabled(updated[idx], a.enabled) + state.copy(customizations = updated) + } + } + } + + is StateActionSessionCustomizationUpdated -> { + val a = action.value + val targetId = customizationId(a.customization) + if (targetId == null) state else { + val list = state.customizations ?: emptyList() + val idx = list.indexOfFirst { customizationId(it) == targetId } + if (idx < 0) state.copy(customizations = list + a.customization) else { + val updated = list.toMutableList() + updated[idx] = a.customization + state.copy(customizations = updated) + } + } + } + + is StateActionSessionCustomizationRemoved -> { + val a = action.value + val list = state.customizations + if (list == null) state else { + val topIdx = list.indexOfFirst { customizationId(it) == a.id } + if (topIdx >= 0) { + val updated = list.toMutableList() + updated.removeAt(topIdx) + state.copy(customizations = updated) + } else { + var changed = false + val updated = list.map { container -> + val children = customizationChildren(container) + if (children == null) container else { + val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } + if (childIdx < 0) container else { + changed = true + val newChildren = children.toMutableList() + newChildren.removeAt(childIdx) + withCustomizationChildren(container, newChildren) + } + } + } + if (!changed) state else state.copy(customizations = updated) + } + } + } + + is StateActionSessionMcpServerStateChanged -> { + val a = action.value + val list = state.customizations + if (list == null) state else { + val topIdx = list.indexOfFirst { customizationId(it) == a.id } + if (topIdx >= 0) { + val entry = list[topIdx] + if (entry !is CustomizationMcpServer) state else { + val updated = list.toMutableList() + updated[topIdx] = CustomizationMcpServer(entry.value.copy(state = a.state, channel = a.channel)) + state.copy(customizations = updated) + } + } else { + var changed = false + val updated = list.map { container -> + val children = customizationChildren(container) + if (children == null) container else { + val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } + if (childIdx < 0) container else { + val child = children[childIdx] + if (child !is ChildCustomizationMcpServer) container else { + changed = true + val newChildren = children.toMutableList() + newChildren[childIdx] = ChildCustomizationMcpServer(child.value.copy(state = a.state, channel = a.channel)) + withCustomizationChildren(container, newChildren) + } + } + } + } + if (!changed) state else state.copy(customizations = updated) + } + } + } + + else -> state +} + +// ─── Chat Reducer ─────────────────────────────────────────────────────────── + +/** Pure reducer for [ChatState]. Handles all chat-channel action variants. */ +public fun chatReducer(state: ChatState, action: StateAction): ChatState = when (action) { + // ── Turn Lifecycle ──────────────────────────────────────────────────── - is StateActionSessionTurnStarted -> { + is StateActionChatTurnStarted -> { val a = action.value val withTurn = state.copy( activeTurn = ActiveTurn( @@ -588,10 +649,8 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat ), ) val withStatus = withTurn.copy( - summary = withTurn.summary.copy( - status = withStatusFlag(summaryStatus(withTurn), SessionStatus.IS_READ, false), - modifiedAt = now(), - ), + status = withStatusFlag(chatSummaryStatus(withTurn), SessionStatus.IS_READ, false), + modifiedAt = nowIsoString(), ) if (a.queuedMessageId == null) { withStatus @@ -609,7 +668,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionDelta -> { + is StateActionChatDelta -> { val a = action.value updateResponsePart(state, a.turnId, a.partId) { part -> if (part is ResponsePartMarkdown) { @@ -620,7 +679,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionResponsePart -> { + is StateActionChatResponsePart -> { val a = action.value val activeTurn = state.activeTurn if (activeTurn == null || activeTurn.id != a.turnId) { @@ -632,18 +691,18 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionTurnComplete -> + is StateActionChatTurnComplete -> endTurn(state, action.value.turnId, TurnState.COMPLETE) - is StateActionSessionTurnCancelled -> + is StateActionChatTurnCancelled -> endTurn(state, action.value.turnId, TurnState.CANCELLED) - is StateActionSessionError -> + is StateActionChatError -> endTurn(state, action.value.turnId, TurnState.ERROR, SessionStatus.ERROR, action.value.error) // ── Tool Call State Machine ─────────────────────────────────────────── - is StateActionSessionToolCallStart -> { + is StateActionChatToolCallStart -> { val a = action.value val activeTurn = state.activeTurn if (activeTurn == null || activeTurn.id != a.turnId) { @@ -668,7 +727,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionToolCallDelta -> { + is StateActionChatToolCallDelta -> { val a = action.value updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStateStreaming) tc else { @@ -683,9 +742,9 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionToolCallReady -> { + is StateActionChatToolCallReady -> { val a = action.value - refreshSummaryStatus( + refreshChatSummaryStatus( updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStateStreaming && tc !is ToolCallStateRunning) { tc @@ -728,9 +787,9 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat ) } - is StateActionSessionToolCallConfirmed -> { + is StateActionChatToolCallConfirmed -> { val a = action.value - refreshSummaryStatus( + refreshChatSummaryStatus( updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStatePendingConfirmation) tc else { val base = toolCallBase(tc).withMeta(a.meta) @@ -774,10 +833,10 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat ) } - is StateActionSessionToolCallComplete -> { + is StateActionChatToolCallComplete -> { val a = action.value val result = a.result - refreshSummaryStatus( + refreshChatSummaryStatus( updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> val (invocationMessage, toolInput, confirmed, selectedOption) = when (tc) { is ToolCallStateRunning -> CompleteCtx( @@ -840,9 +899,9 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat ) } - is StateActionSessionToolCallResultConfirmed -> { + is StateActionChatToolCallResultConfirmed -> { val a = action.value - refreshSummaryStatus( + refreshChatSummaryStatus( updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStatePendingResultConfirmation) tc else { val base = toolCallBase(tc).withMeta(a.meta) @@ -887,7 +946,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat ) } - is StateActionSessionToolCallContentChanged -> { + is StateActionChatToolCallContentChanged -> { val a = action.value updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStateRunning) tc else { @@ -897,12 +956,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } // ── Metadata ────────────────────────────────────────────────────────── - - is StateActionSessionTitleChanged -> state.copy( - summary = state.summary.copy(title = action.value.title, modifiedAt = now()), - ) - - is StateActionSessionUsage -> { + is StateActionChatUsage -> { val a = action.value val activeTurn = state.activeTurn if (activeTurn == null || activeTurn.id != a.turnId) { @@ -912,7 +966,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionReasoning -> { + is StateActionChatReasoning -> { val a = action.value updateResponsePart(state, a.turnId, a.partId) { part -> if (part is ResponsePartReasoning) { @@ -922,220 +976,32 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } } - - is StateActionSessionModelChanged -> state.copy( - summary = state.summary.copy(model = action.value.model, modifiedAt = now()), - ) - - is StateActionSessionAgentChanged -> state.copy( - summary = state.summary.copy(agent = action.value.agent, modifiedAt = now()), - ) - - is StateActionSessionIsReadChanged -> state.copy( - summary = state.summary.copy( - status = withStatusFlag(state.summary.status, SessionStatus.IS_READ, action.value.isRead), - ), - ) - - is StateActionSessionIsArchivedChanged -> state.copy( - summary = state.summary.copy( - status = withStatusFlag(state.summary.status, SessionStatus.IS_ARCHIVED, action.value.isArchived), - ), - ) - - is StateActionSessionActivityChanged -> state.copy( - summary = state.summary.copy(activity = action.value.activity), - ) - - is StateActionSessionChangesetsChanged -> state.copy( - changesets = action.value.changesets, - ) - - is StateActionSessionConfigChanged -> { - val a = action.value - val config = state.config - if (config == null) { - state - } else { - val newValues = if (a.replace == true) a.config else config.values + a.config - state.copy( - config = config.copy(values = newValues), - summary = state.summary.copy(modifiedAt = now()), - ) - } - } - - is StateActionSessionMetaChanged -> state.copy(meta = action.value.meta) - - is StateActionSessionServerToolsChanged -> state.copy(serverTools = action.value.tools) - - is StateActionSessionActiveClientChanged -> state.copy(activeClient = action.value.activeClient) - - is StateActionSessionActiveClientToolsChanged -> { - val client = state.activeClient - if (client == null) { - state - } else { - state.copy(activeClient = client.copy(tools = action.value.tools)) - } - } - - // ── Customizations ──────────────────────────────────────────────────── - - is StateActionSessionCustomizationsChanged -> - state.copy(customizations = action.value.customizations) - - is StateActionSessionCustomizationToggled -> { - val a = action.value - val list = state.customizations - if (list == null) { - state - } else { - val idx = list.indexOfFirst { customizationId(it) == a.id } - if (idx < 0) { - state - } else { - val updated = list.toMutableList() - updated[idx] = withCustomizationEnabled(updated[idx], a.enabled) - state.copy(customizations = updated) - } - } - } - - is StateActionSessionCustomizationUpdated -> { - val a = action.value - // Match Rust: an unknown customization has no id, so we can't locate or - // insert it sensibly — NoOp the update entirely. - val targetId = customizationId(a.customization) - if (targetId == null) { - state - } else { - val list = state.customizations ?: emptyList() - val idx = list.indexOfFirst { customizationId(it) == targetId } - if (idx < 0) { - state.copy(customizations = list + a.customization) - } else { - val updated = list.toMutableList() - updated[idx] = a.customization - state.copy(customizations = updated) - } - } - } - - is StateActionSessionCustomizationRemoved -> { - val a = action.value - val list = state.customizations - if (list == null) { - state - } else { - val topIdx = list.indexOfFirst { customizationId(it) == a.id } - if (topIdx >= 0) { - val updated = list.toMutableList() - updated.removeAt(topIdx) - state.copy(customizations = updated) - } else { - var changed = false - val updated = list.map { container -> - val children = customizationChildren(container) - if (children == null) { - container - } else { - val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } - if (childIdx < 0) { - container - } else { - changed = true - val newChildren = children.toMutableList() - newChildren.removeAt(childIdx) - withCustomizationChildren(container, newChildren) - } - } - } - if (!changed) state else state.copy(customizations = updated) - } - } - } - - is StateActionSessionMcpServerStateChanged -> { - // Full-replacement of an MCP server customization's `state` + `channel`, - // located by id. Mirrors the canonical TS reducer (and the Go/Rust/Swift - // ports): a top-level McpServer entry is matched first (hosts MAY surface - // MCP servers directly at the top level); otherwise the search descends - // into container children. A no-op when no customization carries the id, - // or when the matched id belongs to a non-MCP customization type. - val a = action.value - val list = state.customizations - if (list == null) { - state - } else { - val topIdx = list.indexOfFirst { customizationId(it) == a.id } - if (topIdx >= 0) { - val entry = list[topIdx] - if (entry !is CustomizationMcpServer) { - state - } else { - val updated = list.toMutableList() - updated[topIdx] = CustomizationMcpServer( - entry.value.copy(state = a.state, channel = a.channel), - ) - state.copy(customizations = updated) - } - } else { - var changed = false - val updated = list.map { container -> - val children = customizationChildren(container) - if (children == null) { - container - } else { - val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } - if (childIdx < 0) { - container - } else { - val child = children[childIdx] - if (child !is ChildCustomizationMcpServer) { - container - } else { - changed = true - val newChildren = children.toMutableList() - newChildren[childIdx] = ChildCustomizationMcpServer( - child.value.copy(state = a.state, channel = a.channel), - ) - withCustomizationChildren(container, newChildren) - } - } - } - } - if (!changed) state else state.copy(customizations = updated) - } - } - } - // ── Truncation ──────────────────────────────────────────────────────── - is StateActionSessionTruncated -> { + is StateActionChatTruncated -> { val a = action.value val turns = if (a.turnId == null) { emptyList() } else { val idx = state.turns.indexOfFirst { it.id == a.turnId } - if (idx < 0) return@sessionReducer state + if (idx < 0) return@chatReducer state state.turns.subList(0, idx + 1).toList() } val next = state.copy( turns = turns, activeTurn = null, inputRequests = null, - summary = state.summary.copy(modifiedAt = now()), + modifiedAt = nowIsoString(), ) - next.copy(summary = next.summary.copy(status = summaryStatus(next))) + next.copy(status = chatSummaryStatus(next)) } // ── Session Input Requests ──────────────────────────────────────────── - is StateActionSessionInputRequested -> + is StateActionChatInputRequested -> upsertInputRequest(state, action.value.request) - is StateActionSessionInputAnswerChanged -> { + is StateActionChatInputAnswerChanged -> { val a = action.value val existing = state.inputRequests val idx = existing?.indexOfFirst { it.id == a.requestId } ?: -1 @@ -1153,12 +1019,12 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat val updated = existing.toMutableList().also { it[idx] = newRequest } state.copy( inputRequests = updated, - summary = state.summary.copy(modifiedAt = now()), + modifiedAt = nowIsoString(), ) } } - is StateActionSessionInputCompleted -> { + is StateActionChatInputCompleted -> { val a = action.value val existing = state.inputRequests if (existing == null || existing.none { it.id == a.requestId }) { @@ -1167,17 +1033,15 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat val remaining = existing.filter { it.id != a.requestId } val next = state.copy(inputRequests = remaining.ifEmpty { null }) next.copy( - summary = next.summary.copy( - status = summaryStatus(next), - modifiedAt = now(), - ), + status = chatSummaryStatus(next), + modifiedAt = nowIsoString(), ) } } // ── Pending Messages ────────────────────────────────────────────────── - is StateActionSessionPendingMessageSet -> { + is StateActionChatPendingMessageSet -> { val a = action.value val entry = PendingMessage(id = a.id, message = a.message) if (a.kind == PendingMessageKind.STEERING) { @@ -1194,7 +1058,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionPendingMessageRemoved -> { + is StateActionChatPendingMessageRemoved -> { val a = action.value if (a.kind == PendingMessageKind.STEERING) { val steering = state.steeringMessage @@ -1204,7 +1068,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat state.copy(steeringMessage = null) } } else { - val existing = state.queuedMessages ?: return@sessionReducer state + val existing = state.queuedMessages ?: return@chatReducer state val filtered = existing.filter { it.id != a.id } if (filtered.size == existing.size) { state @@ -1214,9 +1078,9 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } } - is StateActionSessionQueuedMessagesReordered -> { + is StateActionChatQueuedMessagesReordered -> { val a = action.value - val existing = state.queuedMessages ?: return@sessionReducer state + val existing = state.queuedMessages ?: return@chatReducer state val byId = existing.associateBy { it.id } val ordered = LinkedHashSet() val reordered = mutableListOf() @@ -1235,12 +1099,13 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat } else -> state + } /** - * Locally scoped helper for [StateActionSessionToolCallComplete] to avoid - * Pair/Triple noise when carrying the four context fields from the prior - * tool call state into the new one. + * Locally scoped helper for tool-call completion to avoid Pair/Triple noise + * when carrying the four context fields from the prior tool call state into + * the new one. */ private data class CompleteCtx( val invocationMessage: com.microsoft.agenthostprotocol.generated.StringOrMarkdown, @@ -1249,6 +1114,7 @@ private data class CompleteCtx( val selectedOption: ConfirmationOption?, ) + // ─── Terminal Reducer ─────────────────────────────────────────────────────── /** @@ -1455,7 +1321,7 @@ public fun annotationsReducer(state: AnnotationsState, action: StateAction): Ann if (idx < 0) { state } else { - val next: List = state.annotations.toMutableList().also { it.removeAt(idx) } + val next = state.annotations.toMutableList().also { it.removeAt(idx) } state.copy(annotations = next) } } 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 3205f25c..b20eac2f 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 @@ -34,38 +34,46 @@ enum class ActionType { SESSION_READY, @SerialName("session/creationFailed") SESSION_CREATION_FAILED, - @SerialName("session/turnStarted") - SESSION_TURN_STARTED, - @SerialName("session/delta") - SESSION_DELTA, - @SerialName("session/responsePart") - SESSION_RESPONSE_PART, - @SerialName("session/toolCallStart") - SESSION_TOOL_CALL_START, - @SerialName("session/toolCallDelta") - SESSION_TOOL_CALL_DELTA, - @SerialName("session/toolCallReady") - SESSION_TOOL_CALL_READY, - @SerialName("session/toolCallConfirmed") - SESSION_TOOL_CALL_CONFIRMED, - @SerialName("session/toolCallComplete") - SESSION_TOOL_CALL_COMPLETE, - @SerialName("session/toolCallResultConfirmed") - SESSION_TOOL_CALL_RESULT_CONFIRMED, - @SerialName("session/toolCallContentChanged") - SESSION_TOOL_CALL_CONTENT_CHANGED, - @SerialName("session/turnComplete") - SESSION_TURN_COMPLETE, - @SerialName("session/turnCancelled") - SESSION_TURN_CANCELLED, - @SerialName("session/error") - SESSION_ERROR, + @SerialName("session/chatAdded") + SESSION_CHAT_ADDED, + @SerialName("session/chatRemoved") + SESSION_CHAT_REMOVED, + @SerialName("session/chatUpdated") + SESSION_CHAT_UPDATED, + @SerialName("session/defaultChatChanged") + SESSION_DEFAULT_CHAT_CHANGED, + @SerialName("chat/turnStarted") + CHAT_TURN_STARTED, + @SerialName("chat/delta") + CHAT_DELTA, + @SerialName("chat/responsePart") + CHAT_RESPONSE_PART, + @SerialName("chat/toolCallStart") + CHAT_TOOL_CALL_START, + @SerialName("chat/toolCallDelta") + CHAT_TOOL_CALL_DELTA, + @SerialName("chat/toolCallReady") + CHAT_TOOL_CALL_READY, + @SerialName("chat/toolCallConfirmed") + CHAT_TOOL_CALL_CONFIRMED, + @SerialName("chat/toolCallComplete") + CHAT_TOOL_CALL_COMPLETE, + @SerialName("chat/toolCallResultConfirmed") + CHAT_TOOL_CALL_RESULT_CONFIRMED, + @SerialName("chat/toolCallContentChanged") + CHAT_TOOL_CALL_CONTENT_CHANGED, + @SerialName("chat/turnComplete") + CHAT_TURN_COMPLETE, + @SerialName("chat/turnCancelled") + CHAT_TURN_CANCELLED, + @SerialName("chat/error") + CHAT_ERROR, @SerialName("session/titleChanged") SESSION_TITLE_CHANGED, - @SerialName("session/usage") - SESSION_USAGE, - @SerialName("session/reasoning") - SESSION_REASONING, + @SerialName("chat/usage") + CHAT_USAGE, + @SerialName("chat/reasoning") + CHAT_REASONING, @SerialName("session/modelChanged") SESSION_MODEL_CHANGED, @SerialName("session/agentChanged") @@ -76,18 +84,18 @@ enum class ActionType { SESSION_ACTIVE_CLIENT_CHANGED, @SerialName("session/activeClientToolsChanged") SESSION_ACTIVE_CLIENT_TOOLS_CHANGED, - @SerialName("session/pendingMessageSet") - SESSION_PENDING_MESSAGE_SET, - @SerialName("session/pendingMessageRemoved") - SESSION_PENDING_MESSAGE_REMOVED, - @SerialName("session/queuedMessagesReordered") - SESSION_QUEUED_MESSAGES_REORDERED, - @SerialName("session/inputRequested") - SESSION_INPUT_REQUESTED, - @SerialName("session/inputAnswerChanged") - SESSION_INPUT_ANSWER_CHANGED, - @SerialName("session/inputCompleted") - SESSION_INPUT_COMPLETED, + @SerialName("chat/pendingMessageSet") + CHAT_PENDING_MESSAGE_SET, + @SerialName("chat/pendingMessageRemoved") + CHAT_PENDING_MESSAGE_REMOVED, + @SerialName("chat/queuedMessagesReordered") + CHAT_QUEUED_MESSAGES_REORDERED, + @SerialName("chat/inputRequested") + CHAT_INPUT_REQUESTED, + @SerialName("chat/inputAnswerChanged") + CHAT_INPUT_ANSWER_CHANGED, + @SerialName("chat/inputCompleted") + CHAT_INPUT_COMPLETED, @SerialName("session/customizationsChanged") SESSION_CUSTOMIZATIONS_CHANGED, @SerialName("session/customizationToggled") @@ -98,8 +106,8 @@ enum class ActionType { SESSION_CUSTOMIZATION_REMOVED, @SerialName("session/mcpServerStateChanged") SESSION_MCP_SERVER_STATE_CHANGED, - @SerialName("session/truncated") - SESSION_TRUNCATED, + @SerialName("chat/truncated") + CHAT_TRUNCATED, @SerialName("session/isReadChanged") SESSION_IS_READ_CHANGED, @SerialName("session/isArchivedChanged") @@ -219,7 +227,50 @@ data class SessionCreationFailedAction( ) @Serializable -data class SessionTurnStartedAction( +data class SessionChatAddedAction( + val type: ActionType, + /** + * The full summary of the newly added (or upserted) chat. + */ + val summary: ChatSummary +) + +@Serializable +data class SessionChatRemovedAction( + val type: ActionType, + /** + * The URI of the chat to remove. + */ + val chat: String +) + +@Serializable +data class SessionChatUpdatedAction( + val type: ActionType, + /** + * The URI of the chat whose summary changed. + */ + val chat: String, + /** + * Mutable summary fields that changed; omitted fields are unchanged. + * + * Identity fields (`resource`) never change and MUST be omitted by + * senders; receivers SHOULD ignore them if present. + */ + val changes: PartialChatSummary +) + +@Serializable +data class SessionDefaultChatChangedAction( + val type: ActionType, + /** + * New default chat URI, or `undefined` to clear the hint. + */ + val defaultChat: String? = null +) + +@Serializable +data class ChatTurnStartedAction( val type: ActionType, /** * Turn identifier @@ -236,7 +287,7 @@ data class SessionTurnStartedAction( ) @Serializable -data class SessionDeltaAction( +data class ChatDeltaAction( val type: ActionType, /** * Turn identifier @@ -253,7 +304,7 @@ data class SessionDeltaAction( ) @Serializable -data class SessionResponsePartAction( +data class ChatResponsePartAction( val type: ActionType, /** * Turn identifier @@ -266,7 +317,7 @@ data class SessionResponsePartAction( ) @Serializable -data class SessionToolCallStartAction( +data class ChatToolCallStartAction( /** * Turn identifier */ @@ -302,7 +353,7 @@ data class SessionToolCallStartAction( ) @Serializable -data class SessionToolCallDeltaAction( +data class ChatToolCallDeltaAction( /** * Turn identifier */ @@ -333,7 +384,7 @@ data class SessionToolCallDeltaAction( ) @Serializable -data class SessionToolCallReadyAction( +data class ChatToolCallReadyAction( /** * Turn identifier */ @@ -390,9 +441,9 @@ data class SessionToolCallReadyAction( * Client approves or denies a pending tool call (merged approved + denied variants). */ @Serializable -data class SessionToolCallConfirmedAction( +data class ChatToolCallConfirmedAction( /** Action type discriminant */ - val type: ActionType = ActionType.SESSION_TOOL_CALL_CONFIRMED, + val type: ActionType = ActionType.CHAT_TOOL_CALL_CONFIRMED, /** Turn identifier */ val turnId: String, /** Tool call identifier */ @@ -416,7 +467,7 @@ data class SessionToolCallConfirmedAction( ) @Serializable -data class SessionToolCallCompleteAction( +data class ChatToolCallCompleteAction( /** * Turn identifier */ @@ -447,7 +498,7 @@ data class SessionToolCallCompleteAction( ) @Serializable -data class SessionToolCallResultConfirmedAction( +data class ChatToolCallResultConfirmedAction( /** * Turn identifier */ @@ -474,7 +525,34 @@ data class SessionToolCallResultConfirmedAction( ) @Serializable -data class SessionTurnCompleteAction( +data class ChatToolCallContentChangedAction( + /** + * Turn identifier + */ + val turnId: String, + /** + * Tool call identifier + */ + val toolCallId: String, + /** + * Additional provider-specific metadata for this tool call. + * + * Clients MAY look for well-known keys here to provide enhanced UI. + * For example, a `ptyTerminal` key with `{ input: string; output: string }` + * indicates the tool operated on a terminal (both `input` and `output` may + * contain escape sequences). + */ + @SerialName("_meta") + val meta: Map? = null, + val type: ActionType, + /** + * The current partial content for the running tool call + */ + val content: List +) + +@Serializable +data class ChatTurnCompleteAction( val type: ActionType, /** * Turn identifier @@ -483,7 +561,7 @@ data class SessionTurnCompleteAction( ) @Serializable -data class SessionTurnCancelledAction( +data class ChatTurnCancelledAction( val type: ActionType, /** * Turn identifier @@ -492,7 +570,7 @@ data class SessionTurnCancelledAction( ) @Serializable -data class SessionErrorAction( +data class ChatErrorAction( val type: ActionType, /** * Turn identifier @@ -514,7 +592,7 @@ data class SessionTitleChangedAction( ) @Serializable -data class SessionUsageAction( +data class ChatUsageAction( val type: ActionType, /** * Turn identifier @@ -527,7 +605,7 @@ data class SessionUsageAction( ) @Serializable -data class SessionReasoningAction( +data class ChatReasoningAction( val type: ActionType, /** * Turn identifier @@ -626,7 +704,7 @@ data class SessionActiveClientToolsChangedAction( ) @Serializable -data class SessionPendingMessageSetAction( +data class ChatPendingMessageSetAction( val type: ActionType, /** * Whether this is a steering or queued message @@ -643,7 +721,7 @@ data class SessionPendingMessageSetAction( ) @Serializable -data class SessionPendingMessageRemovedAction( +data class ChatPendingMessageRemovedAction( val type: ActionType, /** * Whether this is a steering or queued message @@ -656,7 +734,7 @@ data class SessionPendingMessageRemovedAction( ) @Serializable -data class SessionQueuedMessagesReorderedAction( +data class ChatQueuedMessagesReorderedAction( val type: ActionType, /** * Queued message IDs in the desired order @@ -665,16 +743,16 @@ data class SessionQueuedMessagesReorderedAction( ) @Serializable -data class SessionInputRequestedAction( +data class ChatInputRequestedAction( val type: ActionType, /** * Input request to create or replace */ - val request: SessionInputRequest + val request: ChatInputRequest ) @Serializable -data class SessionInputAnswerChangedAction( +data class ChatInputAnswerChangedAction( val type: ActionType, /** * Input request identifier @@ -687,11 +765,11 @@ data class SessionInputAnswerChangedAction( /** * Updated answer, or `undefined` to clear an answer draft */ - val answer: SessionInputAnswer? = null + val answer: ChatInputAnswer? = null ) @Serializable -data class SessionInputCompletedAction( +data class ChatInputCompletedAction( val type: ActionType, /** * Input request identifier @@ -700,11 +778,11 @@ data class SessionInputCompletedAction( /** * Completion outcome */ - val response: SessionInputResponseKind, + val response: ChatInputResponseKind, /** * Optional final answer replacement, keyed by question ID */ - val answers: Map? = null + val answers: Map? = null ) @Serializable @@ -767,7 +845,7 @@ data class SessionMcpServerStateChangedAction( ) @Serializable -data class SessionTruncatedAction( +data class ChatTruncatedAction( val type: ActionType, /** * Keep turns up to and including this turn. Omit to clear all turns. @@ -798,33 +876,6 @@ data class SessionMetaChangedAction( val meta: Map? = null ) -@Serializable -data class SessionToolCallContentChangedAction( - /** - * Turn identifier - */ - val turnId: String, - /** - * Tool call identifier - */ - val toolCallId: String, - /** - * Additional provider-specific metadata for this tool call. - * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). - */ - @SerialName("_meta") - val meta: Map? = null, - val type: ActionType, - /** - * The current partial content for the running tool call - */ - val content: List -) - @Serializable data class ChangesetStatusChangedAction( val type: ActionType, @@ -1108,6 +1159,52 @@ data class ResourceWatchChangedAction( val changes: JsonElement ) +// ─── Partial Summary Types ────────────────────────────────────────────────── + +@Serializable +data class PartialChatSummary( + /** + * Chat URI + */ + val resource: String? = null, + /** + * Chat title + */ + val title: String? = null, + /** + * Current chat status (reuses SessionStatus shape) + */ + val status: SessionStatus? = null, + /** + * Human-readable description of what the chat is currently doing + */ + val activity: String? = null, + /** + * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + */ + val modifiedAt: String? = null, + /** + * Optional per-chat model override (defaults to the session's model) + */ + val model: ModelSelection? = null, + /** + * Optional per-chat agent override (defaults to the session's agent) + */ + val agent: AgentSelection? = null, + /** + * How this chat came into existence + */ + val origin: ChatOrigin? = null, + /** + * Optional per-chat working directory. + * + * If absent, the chat inherits + * {@link SessionSummary.workingDirectory | the session's working directory}. + * See {@link ChatState.workingDirectory} for usage notes. + */ + val workingDirectory: String? = null +) + // ─── StateAction Union ────────────────────────────────────────────────────── /** @@ -1126,21 +1223,26 @@ sealed interface StateAction @JvmInline value class StateActionRootActiveSessionsChanged(val value: RootActiveSessionsChangedAction) : StateAction @JvmInline value class StateActionSessionReady(val value: SessionReadyAction) : StateAction @JvmInline value class StateActionSessionCreationFailed(val value: SessionCreationFailedAction) : StateAction -@JvmInline value class StateActionSessionTurnStarted(val value: SessionTurnStartedAction) : StateAction -@JvmInline value class StateActionSessionDelta(val value: SessionDeltaAction) : StateAction -@JvmInline value class StateActionSessionResponsePart(val value: SessionResponsePartAction) : StateAction -@JvmInline value class StateActionSessionToolCallStart(val value: SessionToolCallStartAction) : StateAction -@JvmInline value class StateActionSessionToolCallDelta(val value: SessionToolCallDeltaAction) : StateAction -@JvmInline value class StateActionSessionToolCallReady(val value: SessionToolCallReadyAction) : StateAction -@JvmInline value class StateActionSessionToolCallConfirmed(val value: SessionToolCallConfirmedAction) : StateAction -@JvmInline value class StateActionSessionToolCallComplete(val value: SessionToolCallCompleteAction) : StateAction -@JvmInline value class StateActionSessionToolCallResultConfirmed(val value: SessionToolCallResultConfirmedAction) : StateAction -@JvmInline value class StateActionSessionTurnComplete(val value: SessionTurnCompleteAction) : StateAction -@JvmInline value class StateActionSessionTurnCancelled(val value: SessionTurnCancelledAction) : StateAction -@JvmInline value class StateActionSessionError(val value: SessionErrorAction) : StateAction +@JvmInline value class StateActionSessionChatAdded(val value: SessionChatAddedAction) : StateAction +@JvmInline value class StateActionSessionChatRemoved(val value: SessionChatRemovedAction) : StateAction +@JvmInline value class StateActionSessionChatUpdated(val value: SessionChatUpdatedAction) : StateAction +@JvmInline value class StateActionSessionDefaultChatChanged(val value: SessionDefaultChatChangedAction) : StateAction +@JvmInline value class StateActionChatTurnStarted(val value: ChatTurnStartedAction) : StateAction +@JvmInline value class StateActionChatDelta(val value: ChatDeltaAction) : StateAction +@JvmInline value class StateActionChatResponsePart(val value: ChatResponsePartAction) : StateAction +@JvmInline value class StateActionChatToolCallStart(val value: ChatToolCallStartAction) : StateAction +@JvmInline value class StateActionChatToolCallDelta(val value: ChatToolCallDeltaAction) : StateAction +@JvmInline value class StateActionChatToolCallReady(val value: ChatToolCallReadyAction) : StateAction +@JvmInline value class StateActionChatToolCallConfirmed(val value: ChatToolCallConfirmedAction) : StateAction +@JvmInline value class StateActionChatToolCallComplete(val value: ChatToolCallCompleteAction) : StateAction +@JvmInline value class StateActionChatToolCallResultConfirmed(val value: ChatToolCallResultConfirmedAction) : StateAction +@JvmInline value class StateActionChatToolCallContentChanged(val value: ChatToolCallContentChangedAction) : StateAction +@JvmInline value class StateActionChatTurnComplete(val value: ChatTurnCompleteAction) : StateAction +@JvmInline value class StateActionChatTurnCancelled(val value: ChatTurnCancelledAction) : StateAction +@JvmInline value class StateActionChatError(val value: ChatErrorAction) : StateAction @JvmInline value class StateActionSessionTitleChanged(val value: SessionTitleChangedAction) : StateAction -@JvmInline value class StateActionSessionUsage(val value: SessionUsageAction) : StateAction -@JvmInline value class StateActionSessionReasoning(val value: SessionReasoningAction) : StateAction +@JvmInline value class StateActionChatUsage(val value: ChatUsageAction) : StateAction +@JvmInline value class StateActionChatReasoning(val value: ChatReasoningAction) : StateAction @JvmInline value class StateActionSessionModelChanged(val value: SessionModelChangedAction) : StateAction @JvmInline value class StateActionSessionAgentChanged(val value: SessionAgentChangedAction) : StateAction @JvmInline value class StateActionSessionIsReadChanged(val value: SessionIsReadChangedAction) : StateAction @@ -1150,21 +1252,20 @@ sealed interface StateAction @JvmInline value class StateActionSessionServerToolsChanged(val value: SessionServerToolsChangedAction) : StateAction @JvmInline value class StateActionSessionActiveClientChanged(val value: SessionActiveClientChangedAction) : StateAction @JvmInline value class StateActionSessionActiveClientToolsChanged(val value: SessionActiveClientToolsChangedAction) : StateAction -@JvmInline value class StateActionSessionPendingMessageSet(val value: SessionPendingMessageSetAction) : StateAction -@JvmInline value class StateActionSessionPendingMessageRemoved(val value: SessionPendingMessageRemovedAction) : StateAction -@JvmInline value class StateActionSessionQueuedMessagesReordered(val value: SessionQueuedMessagesReorderedAction) : StateAction -@JvmInline value class StateActionSessionInputRequested(val value: SessionInputRequestedAction) : StateAction -@JvmInline value class StateActionSessionInputAnswerChanged(val value: SessionInputAnswerChangedAction) : StateAction -@JvmInline value class StateActionSessionInputCompleted(val value: SessionInputCompletedAction) : 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 +@JvmInline value class StateActionChatInputRequested(val value: ChatInputRequestedAction) : StateAction +@JvmInline value class StateActionChatInputAnswerChanged(val value: ChatInputAnswerChangedAction) : StateAction +@JvmInline value class StateActionChatInputCompleted(val value: ChatInputCompletedAction) : StateAction @JvmInline value class StateActionSessionCustomizationsChanged(val value: SessionCustomizationsChangedAction) : StateAction @JvmInline value class StateActionSessionCustomizationToggled(val value: SessionCustomizationToggledAction) : StateAction @JvmInline value class StateActionSessionCustomizationUpdated(val value: SessionCustomizationUpdatedAction) : StateAction @JvmInline value class StateActionSessionCustomizationRemoved(val value: SessionCustomizationRemovedAction) : StateAction @JvmInline value class StateActionSessionMcpServerStateChanged(val value: SessionMcpServerStateChangedAction) : StateAction -@JvmInline value class StateActionSessionTruncated(val value: SessionTruncatedAction) : StateAction +@JvmInline value class StateActionChatTruncated(val value: ChatTruncatedAction) : StateAction @JvmInline value class StateActionSessionConfigChanged(val value: SessionConfigChangedAction) : StateAction @JvmInline value class StateActionSessionMetaChanged(val value: SessionMetaChangedAction) : StateAction -@JvmInline value class StateActionSessionToolCallContentChanged(val value: SessionToolCallContentChangedAction) : StateAction @JvmInline value class StateActionChangesetStatusChanged(val value: ChangesetStatusChangedAction) : StateAction @JvmInline value class StateActionChangesetFileSet(val value: ChangesetFileSetAction) : StateAction @JvmInline value class StateActionChangesetFileRemoved(val value: ChangesetFileRemovedAction) : StateAction @@ -1209,21 +1310,26 @@ internal object StateActionSerializer : KSerializer { "root/activeSessionsChanged" -> StateActionRootActiveSessionsChanged(input.json.decodeFromJsonElement(RootActiveSessionsChangedAction.serializer(), element)) "session/ready" -> StateActionSessionReady(input.json.decodeFromJsonElement(SessionReadyAction.serializer(), element)) "session/creationFailed" -> StateActionSessionCreationFailed(input.json.decodeFromJsonElement(SessionCreationFailedAction.serializer(), element)) - "session/turnStarted" -> StateActionSessionTurnStarted(input.json.decodeFromJsonElement(SessionTurnStartedAction.serializer(), element)) - "session/delta" -> StateActionSessionDelta(input.json.decodeFromJsonElement(SessionDeltaAction.serializer(), element)) - "session/responsePart" -> StateActionSessionResponsePart(input.json.decodeFromJsonElement(SessionResponsePartAction.serializer(), element)) - "session/toolCallStart" -> StateActionSessionToolCallStart(input.json.decodeFromJsonElement(SessionToolCallStartAction.serializer(), element)) - "session/toolCallDelta" -> StateActionSessionToolCallDelta(input.json.decodeFromJsonElement(SessionToolCallDeltaAction.serializer(), element)) - "session/toolCallReady" -> StateActionSessionToolCallReady(input.json.decodeFromJsonElement(SessionToolCallReadyAction.serializer(), element)) - "session/toolCallConfirmed" -> StateActionSessionToolCallConfirmed(input.json.decodeFromJsonElement(SessionToolCallConfirmedAction.serializer(), element)) - "session/toolCallComplete" -> StateActionSessionToolCallComplete(input.json.decodeFromJsonElement(SessionToolCallCompleteAction.serializer(), element)) - "session/toolCallResultConfirmed" -> StateActionSessionToolCallResultConfirmed(input.json.decodeFromJsonElement(SessionToolCallResultConfirmedAction.serializer(), element)) - "session/turnComplete" -> StateActionSessionTurnComplete(input.json.decodeFromJsonElement(SessionTurnCompleteAction.serializer(), element)) - "session/turnCancelled" -> StateActionSessionTurnCancelled(input.json.decodeFromJsonElement(SessionTurnCancelledAction.serializer(), element)) - "session/error" -> StateActionSessionError(input.json.decodeFromJsonElement(SessionErrorAction.serializer(), element)) + "session/chatAdded" -> StateActionSessionChatAdded(input.json.decodeFromJsonElement(SessionChatAddedAction.serializer(), element)) + "session/chatRemoved" -> StateActionSessionChatRemoved(input.json.decodeFromJsonElement(SessionChatRemovedAction.serializer(), element)) + "session/chatUpdated" -> StateActionSessionChatUpdated(input.json.decodeFromJsonElement(SessionChatUpdatedAction.serializer(), element)) + "session/defaultChatChanged" -> StateActionSessionDefaultChatChanged(input.json.decodeFromJsonElement(SessionDefaultChatChangedAction.serializer(), element)) + "chat/turnStarted" -> StateActionChatTurnStarted(input.json.decodeFromJsonElement(ChatTurnStartedAction.serializer(), element)) + "chat/delta" -> StateActionChatDelta(input.json.decodeFromJsonElement(ChatDeltaAction.serializer(), element)) + "chat/responsePart" -> StateActionChatResponsePart(input.json.decodeFromJsonElement(ChatResponsePartAction.serializer(), element)) + "chat/toolCallStart" -> StateActionChatToolCallStart(input.json.decodeFromJsonElement(ChatToolCallStartAction.serializer(), element)) + "chat/toolCallDelta" -> StateActionChatToolCallDelta(input.json.decodeFromJsonElement(ChatToolCallDeltaAction.serializer(), element)) + "chat/toolCallReady" -> StateActionChatToolCallReady(input.json.decodeFromJsonElement(ChatToolCallReadyAction.serializer(), element)) + "chat/toolCallConfirmed" -> StateActionChatToolCallConfirmed(input.json.decodeFromJsonElement(ChatToolCallConfirmedAction.serializer(), element)) + "chat/toolCallComplete" -> StateActionChatToolCallComplete(input.json.decodeFromJsonElement(ChatToolCallCompleteAction.serializer(), element)) + "chat/toolCallResultConfirmed" -> StateActionChatToolCallResultConfirmed(input.json.decodeFromJsonElement(ChatToolCallResultConfirmedAction.serializer(), element)) + "chat/toolCallContentChanged" -> StateActionChatToolCallContentChanged(input.json.decodeFromJsonElement(ChatToolCallContentChangedAction.serializer(), element)) + "chat/turnComplete" -> StateActionChatTurnComplete(input.json.decodeFromJsonElement(ChatTurnCompleteAction.serializer(), element)) + "chat/turnCancelled" -> StateActionChatTurnCancelled(input.json.decodeFromJsonElement(ChatTurnCancelledAction.serializer(), element)) + "chat/error" -> StateActionChatError(input.json.decodeFromJsonElement(ChatErrorAction.serializer(), element)) "session/titleChanged" -> StateActionSessionTitleChanged(input.json.decodeFromJsonElement(SessionTitleChangedAction.serializer(), element)) - "session/usage" -> StateActionSessionUsage(input.json.decodeFromJsonElement(SessionUsageAction.serializer(), element)) - "session/reasoning" -> StateActionSessionReasoning(input.json.decodeFromJsonElement(SessionReasoningAction.serializer(), element)) + "chat/usage" -> StateActionChatUsage(input.json.decodeFromJsonElement(ChatUsageAction.serializer(), element)) + "chat/reasoning" -> StateActionChatReasoning(input.json.decodeFromJsonElement(ChatReasoningAction.serializer(), element)) "session/modelChanged" -> StateActionSessionModelChanged(input.json.decodeFromJsonElement(SessionModelChangedAction.serializer(), element)) "session/agentChanged" -> StateActionSessionAgentChanged(input.json.decodeFromJsonElement(SessionAgentChangedAction.serializer(), element)) "session/isReadChanged" -> StateActionSessionIsReadChanged(input.json.decodeFromJsonElement(SessionIsReadChangedAction.serializer(), element)) @@ -1233,21 +1339,20 @@ internal object StateActionSerializer : KSerializer { "session/serverToolsChanged" -> StateActionSessionServerToolsChanged(input.json.decodeFromJsonElement(SessionServerToolsChangedAction.serializer(), element)) "session/activeClientChanged" -> StateActionSessionActiveClientChanged(input.json.decodeFromJsonElement(SessionActiveClientChangedAction.serializer(), element)) "session/activeClientToolsChanged" -> StateActionSessionActiveClientToolsChanged(input.json.decodeFromJsonElement(SessionActiveClientToolsChangedAction.serializer(), element)) - "session/pendingMessageSet" -> StateActionSessionPendingMessageSet(input.json.decodeFromJsonElement(SessionPendingMessageSetAction.serializer(), element)) - "session/pendingMessageRemoved" -> StateActionSessionPendingMessageRemoved(input.json.decodeFromJsonElement(SessionPendingMessageRemovedAction.serializer(), element)) - "session/queuedMessagesReordered" -> StateActionSessionQueuedMessagesReordered(input.json.decodeFromJsonElement(SessionQueuedMessagesReorderedAction.serializer(), element)) - "session/inputRequested" -> StateActionSessionInputRequested(input.json.decodeFromJsonElement(SessionInputRequestedAction.serializer(), element)) - "session/inputAnswerChanged" -> StateActionSessionInputAnswerChanged(input.json.decodeFromJsonElement(SessionInputAnswerChangedAction.serializer(), element)) - "session/inputCompleted" -> StateActionSessionInputCompleted(input.json.decodeFromJsonElement(SessionInputCompletedAction.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)) + "chat/inputRequested" -> StateActionChatInputRequested(input.json.decodeFromJsonElement(ChatInputRequestedAction.serializer(), element)) + "chat/inputAnswerChanged" -> StateActionChatInputAnswerChanged(input.json.decodeFromJsonElement(ChatInputAnswerChangedAction.serializer(), element)) + "chat/inputCompleted" -> StateActionChatInputCompleted(input.json.decodeFromJsonElement(ChatInputCompletedAction.serializer(), element)) "session/customizationsChanged" -> StateActionSessionCustomizationsChanged(input.json.decodeFromJsonElement(SessionCustomizationsChangedAction.serializer(), element)) "session/customizationToggled" -> StateActionSessionCustomizationToggled(input.json.decodeFromJsonElement(SessionCustomizationToggledAction.serializer(), element)) "session/customizationUpdated" -> StateActionSessionCustomizationUpdated(input.json.decodeFromJsonElement(SessionCustomizationUpdatedAction.serializer(), element)) "session/customizationRemoved" -> StateActionSessionCustomizationRemoved(input.json.decodeFromJsonElement(SessionCustomizationRemovedAction.serializer(), element)) "session/mcpServerStateChanged" -> StateActionSessionMcpServerStateChanged(input.json.decodeFromJsonElement(SessionMcpServerStateChangedAction.serializer(), element)) - "session/truncated" -> StateActionSessionTruncated(input.json.decodeFromJsonElement(SessionTruncatedAction.serializer(), element)) + "chat/truncated" -> StateActionChatTruncated(input.json.decodeFromJsonElement(ChatTruncatedAction.serializer(), element)) "session/configChanged" -> StateActionSessionConfigChanged(input.json.decodeFromJsonElement(SessionConfigChangedAction.serializer(), element)) "session/metaChanged" -> StateActionSessionMetaChanged(input.json.decodeFromJsonElement(SessionMetaChangedAction.serializer(), element)) - "session/toolCallContentChanged" -> StateActionSessionToolCallContentChanged(input.json.decodeFromJsonElement(SessionToolCallContentChangedAction.serializer(), element)) "changeset/statusChanged" -> StateActionChangesetStatusChanged(input.json.decodeFromJsonElement(ChangesetStatusChangedAction.serializer(), element)) "changeset/fileSet" -> StateActionChangesetFileSet(input.json.decodeFromJsonElement(ChangesetFileSetAction.serializer(), element)) "changeset/fileRemoved" -> StateActionChangesetFileRemoved(input.json.decodeFromJsonElement(ChangesetFileRemovedAction.serializer(), element)) @@ -1285,21 +1390,26 @@ internal object StateActionSerializer : KSerializer { is StateActionRootActiveSessionsChanged -> output.json.encodeToJsonElement(RootActiveSessionsChangedAction.serializer(), value.value) is StateActionSessionReady -> output.json.encodeToJsonElement(SessionReadyAction.serializer(), value.value) is StateActionSessionCreationFailed -> output.json.encodeToJsonElement(SessionCreationFailedAction.serializer(), value.value) - is StateActionSessionTurnStarted -> output.json.encodeToJsonElement(SessionTurnStartedAction.serializer(), value.value) - is StateActionSessionDelta -> output.json.encodeToJsonElement(SessionDeltaAction.serializer(), value.value) - is StateActionSessionResponsePart -> output.json.encodeToJsonElement(SessionResponsePartAction.serializer(), value.value) - is StateActionSessionToolCallStart -> output.json.encodeToJsonElement(SessionToolCallStartAction.serializer(), value.value) - is StateActionSessionToolCallDelta -> output.json.encodeToJsonElement(SessionToolCallDeltaAction.serializer(), value.value) - is StateActionSessionToolCallReady -> output.json.encodeToJsonElement(SessionToolCallReadyAction.serializer(), value.value) - is StateActionSessionToolCallConfirmed -> output.json.encodeToJsonElement(SessionToolCallConfirmedAction.serializer(), value.value) - is StateActionSessionToolCallComplete -> output.json.encodeToJsonElement(SessionToolCallCompleteAction.serializer(), value.value) - is StateActionSessionToolCallResultConfirmed -> output.json.encodeToJsonElement(SessionToolCallResultConfirmedAction.serializer(), value.value) - is StateActionSessionTurnComplete -> output.json.encodeToJsonElement(SessionTurnCompleteAction.serializer(), value.value) - is StateActionSessionTurnCancelled -> output.json.encodeToJsonElement(SessionTurnCancelledAction.serializer(), value.value) - is StateActionSessionError -> output.json.encodeToJsonElement(SessionErrorAction.serializer(), value.value) + is StateActionSessionChatAdded -> output.json.encodeToJsonElement(SessionChatAddedAction.serializer(), value.value) + is StateActionSessionChatRemoved -> output.json.encodeToJsonElement(SessionChatRemovedAction.serializer(), value.value) + is StateActionSessionChatUpdated -> output.json.encodeToJsonElement(SessionChatUpdatedAction.serializer(), value.value) + is StateActionSessionDefaultChatChanged -> output.json.encodeToJsonElement(SessionDefaultChatChangedAction.serializer(), value.value) + is StateActionChatTurnStarted -> output.json.encodeToJsonElement(ChatTurnStartedAction.serializer(), value.value) + is StateActionChatDelta -> output.json.encodeToJsonElement(ChatDeltaAction.serializer(), value.value) + is StateActionChatResponsePart -> output.json.encodeToJsonElement(ChatResponsePartAction.serializer(), value.value) + is StateActionChatToolCallStart -> output.json.encodeToJsonElement(ChatToolCallStartAction.serializer(), value.value) + is StateActionChatToolCallDelta -> output.json.encodeToJsonElement(ChatToolCallDeltaAction.serializer(), value.value) + is StateActionChatToolCallReady -> output.json.encodeToJsonElement(ChatToolCallReadyAction.serializer(), value.value) + is StateActionChatToolCallConfirmed -> output.json.encodeToJsonElement(ChatToolCallConfirmedAction.serializer(), value.value) + is StateActionChatToolCallComplete -> output.json.encodeToJsonElement(ChatToolCallCompleteAction.serializer(), value.value) + is StateActionChatToolCallResultConfirmed -> output.json.encodeToJsonElement(ChatToolCallResultConfirmedAction.serializer(), value.value) + is StateActionChatToolCallContentChanged -> output.json.encodeToJsonElement(ChatToolCallContentChangedAction.serializer(), value.value) + is StateActionChatTurnComplete -> output.json.encodeToJsonElement(ChatTurnCompleteAction.serializer(), value.value) + is StateActionChatTurnCancelled -> output.json.encodeToJsonElement(ChatTurnCancelledAction.serializer(), value.value) + is StateActionChatError -> output.json.encodeToJsonElement(ChatErrorAction.serializer(), value.value) is StateActionSessionTitleChanged -> output.json.encodeToJsonElement(SessionTitleChangedAction.serializer(), value.value) - is StateActionSessionUsage -> output.json.encodeToJsonElement(SessionUsageAction.serializer(), value.value) - is StateActionSessionReasoning -> output.json.encodeToJsonElement(SessionReasoningAction.serializer(), value.value) + is StateActionChatUsage -> output.json.encodeToJsonElement(ChatUsageAction.serializer(), value.value) + is StateActionChatReasoning -> output.json.encodeToJsonElement(ChatReasoningAction.serializer(), value.value) is StateActionSessionModelChanged -> output.json.encodeToJsonElement(SessionModelChangedAction.serializer(), value.value) is StateActionSessionAgentChanged -> output.json.encodeToJsonElement(SessionAgentChangedAction.serializer(), value.value) is StateActionSessionIsReadChanged -> output.json.encodeToJsonElement(SessionIsReadChangedAction.serializer(), value.value) @@ -1309,21 +1419,20 @@ internal object StateActionSerializer : KSerializer { is StateActionSessionServerToolsChanged -> output.json.encodeToJsonElement(SessionServerToolsChangedAction.serializer(), value.value) is StateActionSessionActiveClientChanged -> output.json.encodeToJsonElement(SessionActiveClientChangedAction.serializer(), value.value) is StateActionSessionActiveClientToolsChanged -> output.json.encodeToJsonElement(SessionActiveClientToolsChangedAction.serializer(), value.value) - is StateActionSessionPendingMessageSet -> output.json.encodeToJsonElement(SessionPendingMessageSetAction.serializer(), value.value) - is StateActionSessionPendingMessageRemoved -> output.json.encodeToJsonElement(SessionPendingMessageRemovedAction.serializer(), value.value) - is StateActionSessionQueuedMessagesReordered -> output.json.encodeToJsonElement(SessionQueuedMessagesReorderedAction.serializer(), value.value) - is StateActionSessionInputRequested -> output.json.encodeToJsonElement(SessionInputRequestedAction.serializer(), value.value) - is StateActionSessionInputAnswerChanged -> output.json.encodeToJsonElement(SessionInputAnswerChangedAction.serializer(), value.value) - is StateActionSessionInputCompleted -> output.json.encodeToJsonElement(SessionInputCompletedAction.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) + is StateActionChatInputRequested -> output.json.encodeToJsonElement(ChatInputRequestedAction.serializer(), value.value) + is StateActionChatInputAnswerChanged -> output.json.encodeToJsonElement(ChatInputAnswerChangedAction.serializer(), value.value) + is StateActionChatInputCompleted -> output.json.encodeToJsonElement(ChatInputCompletedAction.serializer(), value.value) is StateActionSessionCustomizationsChanged -> output.json.encodeToJsonElement(SessionCustomizationsChangedAction.serializer(), value.value) is StateActionSessionCustomizationToggled -> output.json.encodeToJsonElement(SessionCustomizationToggledAction.serializer(), value.value) is StateActionSessionCustomizationUpdated -> output.json.encodeToJsonElement(SessionCustomizationUpdatedAction.serializer(), value.value) is StateActionSessionCustomizationRemoved -> output.json.encodeToJsonElement(SessionCustomizationRemovedAction.serializer(), value.value) is StateActionSessionMcpServerStateChanged -> output.json.encodeToJsonElement(SessionMcpServerStateChangedAction.serializer(), value.value) - is StateActionSessionTruncated -> output.json.encodeToJsonElement(SessionTruncatedAction.serializer(), value.value) + is StateActionChatTruncated -> output.json.encodeToJsonElement(ChatTruncatedAction.serializer(), value.value) is StateActionSessionConfigChanged -> output.json.encodeToJsonElement(SessionConfigChangedAction.serializer(), value.value) is StateActionSessionMetaChanged -> output.json.encodeToJsonElement(SessionMetaChangedAction.serializer(), value.value) - is StateActionSessionToolCallContentChanged -> output.json.encodeToJsonElement(SessionToolCallContentChangedAction.serializer(), value.value) is StateActionChangesetStatusChanged -> output.json.encodeToJsonElement(ChangesetStatusChangedAction.serializer(), value.value) is StateActionChangesetFileSet -> output.json.encodeToJsonElement(ChangesetFileSetAction.serializer(), value.value) is StateActionChangesetFileRemoved -> output.json.encodeToJsonElement(ChangesetFileRemovedAction.serializer(), value.value) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index e532cba7..08223dfb 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -330,6 +330,54 @@ data class DisposeSessionParams( val channel: String ) +@Serializable +data class ChatForkSource( + /** + * URI of the existing chat to fork from + */ + val chat: String, + /** + * Turn ID in the source chat; content up to and including this turn's response is copied + */ + val turnId: String +) + +@Serializable +data class CreateChatParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Chat URI (client-chosen, e.g. `ahp-chat:/`). + */ + val chat: String, + /** + * Optional initial message for the new chat. + */ + val initialMessage: Message? = null, + /** + * Optional per-chat model override. + */ + val model: ModelSelection? = null, + /** + * Optional per-chat agent override. + */ + val agent: AgentSelection? = null, + /** + * Optional source chat and turn to fork from. + */ + val source: ChatForkSource? = null +) + +@Serializable +data class DisposeChatParams( + /** + * Channel URI this command targets. + */ + val channel: String +) + @Serializable data class ListSessionsParams( /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index e7128cda..37bd2eea 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -189,7 +189,10 @@ data class PartialSessionSummary( */ val agent: AgentSelection? = null, /** - * The working directory URI for this session + * The default working directory URI for this session. Individual chats + * MAY override via {@link ChatSummary.workingDirectory | their own + * `workingDirectory`}; this field acts as the fallback for any chat that + * does not. */ val workingDirectory: String? = null, /** diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index be4abbfd..c4dda2d1 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 @@ -168,11 +168,21 @@ internal object SessionStatusSerializer : KSerializer { SessionStatus(decoder.decodeInt()) } +@Serializable +enum class ChatOriginKind { + @SerialName("user") + USER, + @SerialName("fork") + FORK, + @SerialName("tool") + TOOL +} + /** * Answer lifecycle state. */ @Serializable -enum class SessionInputAnswerState { +enum class ChatInputAnswerState { @SerialName("draft") DRAFT, @SerialName("submitted") @@ -185,7 +195,7 @@ enum class SessionInputAnswerState { * Answer value kind. */ @Serializable -enum class SessionInputAnswerValueKind { +enum class ChatInputAnswerValueKind { @SerialName("text") TEXT, @SerialName("number") @@ -202,7 +212,7 @@ enum class SessionInputAnswerValueKind { * Question/input control kind. */ @Serializable -enum class SessionInputQuestionKind { +enum class ChatInputQuestionKind { @SerialName("text") TEXT, @SerialName("number") @@ -221,7 +231,7 @@ enum class SessionInputQuestionKind { * How a client completed an input request. */ @Serializable -enum class SessionInputResponseKind { +enum class ChatInputResponseKind { @SerialName("accept") ACCEPT, @SerialName("decline") @@ -939,27 +949,49 @@ data class PendingMessage( ) @Serializable -data class SessionState( +data class ChatState( /** - * Lightweight session metadata + * Chat URI */ - val summary: SessionSummary, + val resource: String, /** - * Session initialization state + * Chat title */ - val lifecycle: SessionLifecycle, + val title: String, /** - * Error details if creation failed + * Current chat status (reuses SessionStatus shape) */ - val creationError: ErrorInfo? = null, + val status: SessionStatus, /** - * Tools provided by the server (agent host) for this session + * Human-readable description of what the chat is currently doing */ - val serverTools: List? = null, + val activity: String? = null, /** - * The client currently providing tools and interactive capabilities to this session + * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ - val activeClient: SessionActiveClient? = null, + val modifiedAt: String, + /** + * Optional per-chat model override (defaults to the session's model) + */ + val model: ModelSelection? = null, + /** + * Optional per-chat agent override (defaults to the session's agent) + */ + val agent: AgentSelection? = null, + /** + * How this chat came into existence + */ + val origin: ChatOrigin? = null, + /** + * Optional per-chat working directory. + * + * If absent, the chat inherits + * {@link SessionSummary.workingDirectory | the session's working directory}. + * Hosts MAY override this for individual chats — for example, to give a + * subordinate chat its own git worktree so multiple chats in a session can + * make independent edits that the orchestrator later merges back. + */ + val workingDirectory: String? = null, /** * Completed turns */ @@ -977,9 +1009,93 @@ data class SessionState( */ val queuedMessages: List? = null, /** - * Requests for user input that are currently blocking or informing session progress + * Requests for user input that are currently blocking or informing chat progress + */ + val inputRequests: List? = null, + /** + * Additional provider-specific metadata for this chat. + */ + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class ChatSummary( + /** + * Chat URI + */ + val resource: String, + /** + * Chat title + */ + val title: String, + /** + * Current chat status (reuses SessionStatus shape) + */ + val status: SessionStatus, + /** + * Human-readable description of what the chat is currently doing + */ + val activity: String? = null, + /** + * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + */ + val modifiedAt: String, + /** + * Optional per-chat model override (defaults to the session's model) + */ + val model: ModelSelection? = null, + /** + * Optional per-chat agent override (defaults to the session's agent) */ - val inputRequests: List? = null, + val agent: AgentSelection? = null, + /** + * How this chat came into existence + */ + val origin: ChatOrigin? = null, + /** + * Optional per-chat working directory. + * + * If absent, the chat inherits + * {@link SessionSummary.workingDirectory | the session's working directory}. + * See {@link ChatState.workingDirectory} for usage notes. + */ + val workingDirectory: String? = null +) + +@Serializable +data class SessionState( + /** + * Lightweight session metadata + */ + val summary: SessionSummary, + /** + * Session initialization state + */ + val lifecycle: SessionLifecycle, + /** + * Error details if creation failed + */ + val creationError: ErrorInfo? = null, + /** + * Tools provided by the server (agent host) for this session + */ + val serverTools: List? = null, + /** + * The client currently providing tools and interactive capabilities to this session + */ + val activeClient: SessionActiveClient? = null, + /** + * Catalog of chats in this session. + */ + val chats: List, + /** + * The chat that receives input when the user addresses the session without + * selecting a specific chat. This is a UI routing hint, not a hierarchy + * marker — chats remain equal peers at the protocol level. Hosts MAY change + * this over the session's lifetime. + */ + val defaultChat: String? = null, /** * Session configuration schema and current values */ @@ -1096,7 +1212,10 @@ data class SessionSummary( */ val agent: AgentSelection? = null, /** - * The working directory URI for this session + * The default working directory URI for this session. Individual chats + * MAY override via {@link ChatSummary.workingDirectory | their own + * `workingDirectory`}; this field acts as the fallback for any chat that + * does not. */ val workingDirectory: String? = null, /** @@ -1234,7 +1353,7 @@ data class Message( ) @Serializable -data class SessionInputOption( +data class ChatInputOption( /** * Stable option identifier; for MCP enum values this is the enum string */ @@ -1254,26 +1373,26 @@ data class SessionInputOption( ) @Serializable -data class SessionInputTextAnswerValue( - val kind: SessionInputAnswerValueKind, +data class ChatInputTextAnswerValue( + val kind: ChatInputAnswerValueKind, val value: String ) @Serializable -data class SessionInputNumberAnswerValue( - val kind: SessionInputAnswerValueKind, +data class ChatInputNumberAnswerValue( + val kind: ChatInputAnswerValueKind, val value: Double ) @Serializable -data class SessionInputBooleanAnswerValue( - val kind: SessionInputAnswerValueKind, +data class ChatInputBooleanAnswerValue( + val kind: ChatInputAnswerValueKind, val value: Boolean ) @Serializable -data class SessionInputSelectedAnswerValue( - val kind: SessionInputAnswerValueKind, +data class ChatInputSelectedAnswerValue( + val kind: ChatInputAnswerValueKind, val value: String, /** * Free-form text entered instead of selecting an option @@ -1282,8 +1401,8 @@ data class SessionInputSelectedAnswerValue( ) @Serializable -data class SessionInputSelectedManyAnswerValue( - val kind: SessionInputAnswerValueKind, +data class ChatInputSelectedManyAnswerValue( + val kind: ChatInputAnswerValueKind, val value: List, /** * Free-form text entered in addition to selected options @@ -1292,23 +1411,23 @@ data class SessionInputSelectedManyAnswerValue( ) @Serializable -data class SessionInputAnswered( +data class ChatInputAnswered( /** * Answer state */ - val state: SessionInputAnswerState, + val state: ChatInputAnswerState, /** * Answer value */ - val value: SessionInputAnswerValue + val value: ChatInputAnswerValue ) @Serializable -data class SessionInputSkipped( +data class ChatInputSkipped( /** * Answer state */ - val state: SessionInputAnswerState, + val state: ChatInputAnswerState, /** * Free-form reason or value captured while skipping, if any */ @@ -1316,7 +1435,7 @@ data class SessionInputSkipped( ) @Serializable -data class SessionInputTextQuestion( +data class ChatInputTextQuestion( /** * Stable question identifier used as the key in `answers` */ @@ -1333,7 +1452,7 @@ data class SessionInputTextQuestion( * Whether the user must answer this question to accept the request */ val required: Boolean? = null, - val kind: SessionInputQuestionKind, + val kind: ChatInputQuestionKind, /** * Format hint for text questions, such as `email`, `uri`, `date`, or `date-time` */ @@ -1353,7 +1472,7 @@ data class SessionInputTextQuestion( ) @Serializable -data class SessionInputNumberQuestion( +data class ChatInputNumberQuestion( /** * Stable question identifier used as the key in `answers` */ @@ -1370,7 +1489,7 @@ data class SessionInputNumberQuestion( * Whether the user must answer this question to accept the request */ val required: Boolean? = null, - val kind: SessionInputQuestionKind, + val kind: ChatInputQuestionKind, /** * Minimum value */ @@ -1386,7 +1505,7 @@ data class SessionInputNumberQuestion( ) @Serializable -data class SessionInputBooleanQuestion( +data class ChatInputBooleanQuestion( /** * Stable question identifier used as the key in `answers` */ @@ -1403,7 +1522,7 @@ data class SessionInputBooleanQuestion( * Whether the user must answer this question to accept the request */ val required: Boolean? = null, - val kind: SessionInputQuestionKind, + val kind: ChatInputQuestionKind, /** * Default boolean value */ @@ -1411,7 +1530,7 @@ data class SessionInputBooleanQuestion( ) @Serializable -data class SessionInputSingleSelectQuestion( +data class ChatInputSingleSelectQuestion( /** * Stable question identifier used as the key in `answers` */ @@ -1428,11 +1547,11 @@ data class SessionInputSingleSelectQuestion( * Whether the user must answer this question to accept the request */ val required: Boolean? = null, - val kind: SessionInputQuestionKind, + val kind: ChatInputQuestionKind, /** * Options the user may select from */ - val options: List, + val options: List, /** * Whether the user may enter text instead of selecting an option */ @@ -1440,7 +1559,7 @@ data class SessionInputSingleSelectQuestion( ) @Serializable -data class SessionInputMultiSelectQuestion( +data class ChatInputMultiSelectQuestion( /** * Stable question identifier used as the key in `answers` */ @@ -1457,11 +1576,11 @@ data class SessionInputMultiSelectQuestion( * Whether the user must answer this question to accept the request */ val required: Boolean? = null, - val kind: SessionInputQuestionKind, + val kind: ChatInputQuestionKind, /** * Options the user may select from */ - val options: List, + val options: List, /** * Whether the user may enter text in addition to selecting options */ @@ -1477,7 +1596,7 @@ data class SessionInputMultiSelectQuestion( ) @Serializable -data class SessionInputRequest( +data class ChatInputRequest( /** * Stable request identifier */ @@ -1493,11 +1612,11 @@ data class SessionInputRequest( /** * Ordered questions to ask the user */ - val questions: List? = null, + val questions: List? = null, /** * Current draft or submitted answers, keyed by question ID */ - val answers: Map? = null + val answers: Map? = null ) @Serializable @@ -1754,7 +1873,7 @@ data class MarkdownResponsePart( */ val kind: ResponsePartKind, /** - * Part identifier, used by `session/delta` to target this part for content appends + * Part identifier, used by `chat/delta` to target this part for content appends */ val id: String, /** @@ -1818,7 +1937,7 @@ data class ReasoningResponsePart( */ val kind: ResponsePartKind, /** - * Part identifier, used by `session/reasoning` to target this part for content appends + * Part identifier, used by `chat/reasoning` to target this part for content appends */ val id: String, /** @@ -2982,7 +3101,7 @@ data class ToolCallClientContributor( * Absent for server-side tools. * * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. + * dispatching `chat/toolCallComplete` with the result. */ val clientId: String ) @@ -3199,7 +3318,7 @@ data class ErrorInfo( @Serializable data class Snapshot( /** - * The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`) + * The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`) */ val resource: String, /** @@ -3521,6 +3640,60 @@ data class ResourceChange( // ─── Discriminated Unions ─────────────────────────────────────────────────── +@Serializable(with = ChatOriginSerializer::class) +sealed interface ChatOrigin { + @JvmInline value class User(val value: ChatOriginUser) : ChatOrigin + @JvmInline value class Fork(val value: ChatOriginFork) : ChatOrigin + @JvmInline value class Tool(val value: ChatOriginTool) : ChatOrigin + @JvmInline value class Unknown(val raw: JsonObject) : ChatOrigin +} + +@Serializable +data class ChatOriginUser( + val kind: ChatOriginKind = ChatOriginKind.USER, +) + +@Serializable +data class ChatOriginFork( + val kind: ChatOriginKind = ChatOriginKind.FORK, + val chat: String, + val turnId: String, +) + +@Serializable +data class ChatOriginTool( + val kind: ChatOriginKind = ChatOriginKind.TOOL, + val chat: String, + val toolCallId: String, +) + +internal object ChatOriginSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ChatOrigin") + + override fun deserialize(decoder: Decoder): ChatOrigin { + val input = decoder as? JsonDecoder ?: error("ChatOrigin can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject ?: error("Expected JsonObject for ChatOrigin") + return when ((obj["kind"] as? JsonPrimitive)?.contentOrNull) { + "user" -> ChatOrigin.User(input.json.decodeFromJsonElement(ChatOriginUser.serializer(), element)) + "fork" -> ChatOrigin.Fork(input.json.decodeFromJsonElement(ChatOriginFork.serializer(), element)) + "tool" -> ChatOrigin.Tool(input.json.decodeFromJsonElement(ChatOriginTool.serializer(), element)) + else -> ChatOrigin.Unknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: ChatOrigin) { + val output = encoder as? JsonEncoder ?: error("ChatOrigin can only be serialized to JSON") + val element: JsonElement = when (value) { + is ChatOrigin.User -> output.json.encodeToJsonElement(ChatOriginUser.serializer(), value.value) + is ChatOrigin.Fork -> output.json.encodeToJsonElement(ChatOriginFork.serializer(), value.value) + is ChatOrigin.Tool -> output.json.encodeToJsonElement(ChatOriginTool.serializer(), value.value) + is ChatOrigin.Unknown -> value.raw + } + output.encodeJsonElement(element) + } +} + @Serializable(with = ResponsePartSerializer::class) sealed interface ResponsePart @@ -3745,21 +3918,21 @@ internal object TerminalContentPartSerializer : KSerializer } } -@Serializable(with = SessionInputQuestionSerializer::class) -sealed interface SessionInputQuestion +@Serializable(with = ChatInputQuestionSerializer::class) +sealed interface ChatInputQuestion @JvmInline -value class SessionInputQuestionText(val value: SessionInputTextQuestion) : SessionInputQuestion +value class ChatInputQuestionText(val value: ChatInputTextQuestion) : ChatInputQuestion @JvmInline -value class SessionInputQuestionNumber(val value: SessionInputNumberQuestion) : SessionInputQuestion +value class ChatInputQuestionNumber(val value: ChatInputNumberQuestion) : ChatInputQuestion @JvmInline -value class SessionInputQuestionBoolean(val value: SessionInputBooleanQuestion) : SessionInputQuestion +value class ChatInputQuestionBoolean(val value: ChatInputBooleanQuestion) : ChatInputQuestion @JvmInline -value class SessionInputQuestionSingleSelect(val value: SessionInputSingleSelectQuestion) : SessionInputQuestion +value class ChatInputQuestionSingleSelect(val value: ChatInputSingleSelectQuestion) : ChatInputQuestion @JvmInline -value class SessionInputQuestionMultiSelect(val value: SessionInputMultiSelectQuestion) : SessionInputQuestion +value class ChatInputQuestionMultiSelect(val value: ChatInputMultiSelectQuestion) : ChatInputQuestion /** - * Forward-compat catch-all for unknown SessionInputQuestion discriminators. + * Forward-compat catch-all for unknown ChatInputQuestion 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. @@ -3767,61 +3940,61 @@ value class SessionInputQuestionMultiSelect(val value: SessionInputMultiSelectQu * as a no-op, but see `Reducers.kt` for the exact treatment). */ @JvmInline -value class SessionInputQuestionUnknown(val raw: JsonObject) : SessionInputQuestion +value class ChatInputQuestionUnknown(val raw: JsonObject) : ChatInputQuestion -internal object SessionInputQuestionSerializer : KSerializer { +internal object ChatInputQuestionSerializer : KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("SessionInputQuestion") + buildClassSerialDescriptor("ChatInputQuestion") - override fun deserialize(decoder: Decoder): SessionInputQuestion { + override fun deserialize(decoder: Decoder): ChatInputQuestion { val input = decoder as? JsonDecoder - ?: error("SessionInputQuestion can only be deserialized from JSON") + ?: error("ChatInputQuestion can only be deserialized from JSON") val element = input.decodeJsonElement() val obj = element as? JsonObject - ?: error("Expected JsonObject for SessionInputQuestion") + ?: error("Expected JsonObject for ChatInputQuestion") val discriminant = (obj["kind"] as? JsonPrimitive)?.content - ?: return SessionInputQuestionUnknown(obj) + ?: return ChatInputQuestionUnknown(obj) return when (discriminant) { - "text" -> SessionInputQuestionText(input.json.decodeFromJsonElement(SessionInputTextQuestion.serializer(), element)) - "number" -> SessionInputQuestionNumber(input.json.decodeFromJsonElement(SessionInputNumberQuestion.serializer(), element)) - "integer" -> SessionInputQuestionNumber(input.json.decodeFromJsonElement(SessionInputNumberQuestion.serializer(), element)) - "boolean" -> SessionInputQuestionBoolean(input.json.decodeFromJsonElement(SessionInputBooleanQuestion.serializer(), element)) - "single-select" -> SessionInputQuestionSingleSelect(input.json.decodeFromJsonElement(SessionInputSingleSelectQuestion.serializer(), element)) - "multi-select" -> SessionInputQuestionMultiSelect(input.json.decodeFromJsonElement(SessionInputMultiSelectQuestion.serializer(), element)) - else -> SessionInputQuestionUnknown(obj) + "text" -> ChatInputQuestionText(input.json.decodeFromJsonElement(ChatInputTextQuestion.serializer(), element)) + "number" -> ChatInputQuestionNumber(input.json.decodeFromJsonElement(ChatInputNumberQuestion.serializer(), element)) + "integer" -> ChatInputQuestionNumber(input.json.decodeFromJsonElement(ChatInputNumberQuestion.serializer(), element)) + "boolean" -> ChatInputQuestionBoolean(input.json.decodeFromJsonElement(ChatInputBooleanQuestion.serializer(), element)) + "single-select" -> ChatInputQuestionSingleSelect(input.json.decodeFromJsonElement(ChatInputSingleSelectQuestion.serializer(), element)) + "multi-select" -> ChatInputQuestionMultiSelect(input.json.decodeFromJsonElement(ChatInputMultiSelectQuestion.serializer(), element)) + else -> ChatInputQuestionUnknown(obj) } } - override fun serialize(encoder: Encoder, value: SessionInputQuestion) { + override fun serialize(encoder: Encoder, value: ChatInputQuestion) { val output = encoder as? JsonEncoder - ?: error("SessionInputQuestion can only be serialized to JSON") + ?: error("ChatInputQuestion can only be serialized to JSON") val element: JsonElement = when (value) { - is SessionInputQuestionText -> output.json.encodeToJsonElement(SessionInputTextQuestion.serializer(), value.value) - is SessionInputQuestionNumber -> output.json.encodeToJsonElement(SessionInputNumberQuestion.serializer(), value.value) - is SessionInputQuestionBoolean -> output.json.encodeToJsonElement(SessionInputBooleanQuestion.serializer(), value.value) - is SessionInputQuestionSingleSelect -> output.json.encodeToJsonElement(SessionInputSingleSelectQuestion.serializer(), value.value) - is SessionInputQuestionMultiSelect -> output.json.encodeToJsonElement(SessionInputMultiSelectQuestion.serializer(), value.value) - is SessionInputQuestionUnknown -> value.raw + is ChatInputQuestionText -> output.json.encodeToJsonElement(ChatInputTextQuestion.serializer(), value.value) + is ChatInputQuestionNumber -> output.json.encodeToJsonElement(ChatInputNumberQuestion.serializer(), value.value) + is ChatInputQuestionBoolean -> output.json.encodeToJsonElement(ChatInputBooleanQuestion.serializer(), value.value) + is ChatInputQuestionSingleSelect -> output.json.encodeToJsonElement(ChatInputSingleSelectQuestion.serializer(), value.value) + is ChatInputQuestionMultiSelect -> output.json.encodeToJsonElement(ChatInputMultiSelectQuestion.serializer(), value.value) + is ChatInputQuestionUnknown -> value.raw } output.encodeJsonElement(element) } } -@Serializable(with = SessionInputAnswerValueSerializer::class) -sealed interface SessionInputAnswerValue +@Serializable(with = ChatInputAnswerValueSerializer::class) +sealed interface ChatInputAnswerValue @JvmInline -value class SessionInputAnswerValueText(val value: SessionInputTextAnswerValue) : SessionInputAnswerValue +value class ChatInputAnswerValueText(val value: ChatInputTextAnswerValue) : ChatInputAnswerValue @JvmInline -value class SessionInputAnswerValueNumber(val value: SessionInputNumberAnswerValue) : SessionInputAnswerValue +value class ChatInputAnswerValueNumber(val value: ChatInputNumberAnswerValue) : ChatInputAnswerValue @JvmInline -value class SessionInputAnswerValueBoolean(val value: SessionInputBooleanAnswerValue) : SessionInputAnswerValue +value class ChatInputAnswerValueBoolean(val value: ChatInputBooleanAnswerValue) : ChatInputAnswerValue @JvmInline -value class SessionInputAnswerValueSelected(val value: SessionInputSelectedAnswerValue) : SessionInputAnswerValue +value class ChatInputAnswerValueSelected(val value: ChatInputSelectedAnswerValue) : ChatInputAnswerValue @JvmInline -value class SessionInputAnswerValueSelectedMany(val value: SessionInputSelectedManyAnswerValue) : SessionInputAnswerValue +value class ChatInputAnswerValueSelectedMany(val value: ChatInputSelectedManyAnswerValue) : ChatInputAnswerValue /** - * Forward-compat catch-all for unknown SessionInputAnswerValue discriminators. + * Forward-compat catch-all for unknown ChatInputAnswerValue 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. @@ -3829,54 +4002,54 @@ value class SessionInputAnswerValueSelectedMany(val value: SessionInputSelectedM * as a no-op, but see `Reducers.kt` for the exact treatment). */ @JvmInline -value class SessionInputAnswerValueUnknown(val raw: JsonObject) : SessionInputAnswerValue +value class ChatInputAnswerValueUnknown(val raw: JsonObject) : ChatInputAnswerValue -internal object SessionInputAnswerValueSerializer : KSerializer { +internal object ChatInputAnswerValueSerializer : KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("SessionInputAnswerValue") + buildClassSerialDescriptor("ChatInputAnswerValue") - override fun deserialize(decoder: Decoder): SessionInputAnswerValue { + override fun deserialize(decoder: Decoder): ChatInputAnswerValue { val input = decoder as? JsonDecoder - ?: error("SessionInputAnswerValue can only be deserialized from JSON") + ?: error("ChatInputAnswerValue can only be deserialized from JSON") val element = input.decodeJsonElement() val obj = element as? JsonObject - ?: error("Expected JsonObject for SessionInputAnswerValue") + ?: error("Expected JsonObject for ChatInputAnswerValue") val discriminant = (obj["kind"] as? JsonPrimitive)?.content - ?: return SessionInputAnswerValueUnknown(obj) + ?: return ChatInputAnswerValueUnknown(obj) return when (discriminant) { - "text" -> SessionInputAnswerValueText(input.json.decodeFromJsonElement(SessionInputTextAnswerValue.serializer(), element)) - "number" -> SessionInputAnswerValueNumber(input.json.decodeFromJsonElement(SessionInputNumberAnswerValue.serializer(), element)) - "boolean" -> SessionInputAnswerValueBoolean(input.json.decodeFromJsonElement(SessionInputBooleanAnswerValue.serializer(), element)) - "selected" -> SessionInputAnswerValueSelected(input.json.decodeFromJsonElement(SessionInputSelectedAnswerValue.serializer(), element)) - "selected-many" -> SessionInputAnswerValueSelectedMany(input.json.decodeFromJsonElement(SessionInputSelectedManyAnswerValue.serializer(), element)) - else -> SessionInputAnswerValueUnknown(obj) + "text" -> ChatInputAnswerValueText(input.json.decodeFromJsonElement(ChatInputTextAnswerValue.serializer(), element)) + "number" -> ChatInputAnswerValueNumber(input.json.decodeFromJsonElement(ChatInputNumberAnswerValue.serializer(), element)) + "boolean" -> ChatInputAnswerValueBoolean(input.json.decodeFromJsonElement(ChatInputBooleanAnswerValue.serializer(), element)) + "selected" -> ChatInputAnswerValueSelected(input.json.decodeFromJsonElement(ChatInputSelectedAnswerValue.serializer(), element)) + "selected-many" -> ChatInputAnswerValueSelectedMany(input.json.decodeFromJsonElement(ChatInputSelectedManyAnswerValue.serializer(), element)) + else -> ChatInputAnswerValueUnknown(obj) } } - override fun serialize(encoder: Encoder, value: SessionInputAnswerValue) { + override fun serialize(encoder: Encoder, value: ChatInputAnswerValue) { val output = encoder as? JsonEncoder - ?: error("SessionInputAnswerValue can only be serialized to JSON") + ?: error("ChatInputAnswerValue can only be serialized to JSON") val element: JsonElement = when (value) { - is SessionInputAnswerValueText -> output.json.encodeToJsonElement(SessionInputTextAnswerValue.serializer(), value.value) - is SessionInputAnswerValueNumber -> output.json.encodeToJsonElement(SessionInputNumberAnswerValue.serializer(), value.value) - is SessionInputAnswerValueBoolean -> output.json.encodeToJsonElement(SessionInputBooleanAnswerValue.serializer(), value.value) - is SessionInputAnswerValueSelected -> output.json.encodeToJsonElement(SessionInputSelectedAnswerValue.serializer(), value.value) - is SessionInputAnswerValueSelectedMany -> output.json.encodeToJsonElement(SessionInputSelectedManyAnswerValue.serializer(), value.value) - is SessionInputAnswerValueUnknown -> value.raw + is ChatInputAnswerValueText -> output.json.encodeToJsonElement(ChatInputTextAnswerValue.serializer(), value.value) + is ChatInputAnswerValueNumber -> output.json.encodeToJsonElement(ChatInputNumberAnswerValue.serializer(), value.value) + is ChatInputAnswerValueBoolean -> output.json.encodeToJsonElement(ChatInputBooleanAnswerValue.serializer(), value.value) + is ChatInputAnswerValueSelected -> output.json.encodeToJsonElement(ChatInputSelectedAnswerValue.serializer(), value.value) + is ChatInputAnswerValueSelectedMany -> output.json.encodeToJsonElement(ChatInputSelectedManyAnswerValue.serializer(), value.value) + is ChatInputAnswerValueUnknown -> value.raw } output.encodeJsonElement(element) } } -@Serializable(with = SessionInputAnswerSerializer::class) -sealed interface SessionInputAnswer +@Serializable(with = ChatInputAnswerSerializer::class) +sealed interface ChatInputAnswer @JvmInline -value class SessionInputAnswerDraft(val value: SessionInputAnswered) : SessionInputAnswer +value class ChatInputAnswerDraft(val value: ChatInputAnswered) : ChatInputAnswer @JvmInline -value class SessionInputAnswerSkipped(val value: SessionInputSkipped) : SessionInputAnswer +value class ChatInputAnswerSkipped(val value: ChatInputSkipped) : ChatInputAnswer /** - * Forward-compat catch-all for unknown SessionInputAnswer discriminators. + * Forward-compat catch-all for unknown ChatInputAnswer 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. @@ -3884,35 +4057,35 @@ value class SessionInputAnswerSkipped(val value: SessionInputSkipped) : SessionI * as a no-op, but see `Reducers.kt` for the exact treatment). */ @JvmInline -value class SessionInputAnswerUnknown(val raw: JsonObject) : SessionInputAnswer +value class ChatInputAnswerUnknown(val raw: JsonObject) : ChatInputAnswer -internal object SessionInputAnswerSerializer : KSerializer { +internal object ChatInputAnswerSerializer : KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("SessionInputAnswer") + buildClassSerialDescriptor("ChatInputAnswer") - override fun deserialize(decoder: Decoder): SessionInputAnswer { + override fun deserialize(decoder: Decoder): ChatInputAnswer { val input = decoder as? JsonDecoder - ?: error("SessionInputAnswer can only be deserialized from JSON") + ?: error("ChatInputAnswer can only be deserialized from JSON") val element = input.decodeJsonElement() val obj = element as? JsonObject - ?: error("Expected JsonObject for SessionInputAnswer") + ?: error("Expected JsonObject for ChatInputAnswer") val discriminant = (obj["state"] as? JsonPrimitive)?.content - ?: return SessionInputAnswerUnknown(obj) + ?: return ChatInputAnswerUnknown(obj) return when (discriminant) { - "draft" -> SessionInputAnswerDraft(input.json.decodeFromJsonElement(SessionInputAnswered.serializer(), element)) - "submitted" -> SessionInputAnswerDraft(input.json.decodeFromJsonElement(SessionInputAnswered.serializer(), element)) - "skipped" -> SessionInputAnswerSkipped(input.json.decodeFromJsonElement(SessionInputSkipped.serializer(), element)) - else -> SessionInputAnswerUnknown(obj) + "draft" -> ChatInputAnswerDraft(input.json.decodeFromJsonElement(ChatInputAnswered.serializer(), element)) + "submitted" -> ChatInputAnswerDraft(input.json.decodeFromJsonElement(ChatInputAnswered.serializer(), element)) + "skipped" -> ChatInputAnswerSkipped(input.json.decodeFromJsonElement(ChatInputSkipped.serializer(), element)) + else -> ChatInputAnswerUnknown(obj) } } - override fun serialize(encoder: Encoder, value: SessionInputAnswer) { + override fun serialize(encoder: Encoder, value: ChatInputAnswer) { val output = encoder as? JsonEncoder - ?: error("SessionInputAnswer can only be serialized to JSON") + ?: error("ChatInputAnswer can only be serialized to JSON") val element: JsonElement = when (value) { - is SessionInputAnswerDraft -> output.json.encodeToJsonElement(SessionInputAnswered.serializer(), value.value) - is SessionInputAnswerSkipped -> output.json.encodeToJsonElement(SessionInputSkipped.serializer(), value.value) - is SessionInputAnswerUnknown -> value.raw + is ChatInputAnswerDraft -> output.json.encodeToJsonElement(ChatInputAnswered.serializer(), value.value) + is ChatInputAnswerSkipped -> output.json.encodeToJsonElement(ChatInputSkipped.serializer(), value.value) + is ChatInputAnswerUnknown -> value.raw } output.encodeJsonElement(element) } @@ -4318,13 +4491,14 @@ internal object ToolResultContentSerializer : KSerializer { } /** - * The state payload of a snapshot — root, session, terminal, changeset, + * The state payload of a snapshot — root, session, chat, terminal, changeset, * resource-watch, or annotations state. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { @JvmInline value class Root(val value: RootState) : SnapshotState @JvmInline value class Session(val value: SessionState) : SnapshotState + @JvmInline value class Chat(val value: ChatState) : SnapshotState @JvmInline value class Terminal(val value: TerminalState) : SnapshotState @JvmInline value class Changeset(val value: ChangesetState) : SnapshotState @JvmInline value class ResourceWatch(val value: ResourceWatchState) : SnapshotState @@ -4366,6 +4540,7 @@ internal object SnapshotStateSerializer : KSerializer { val element: JsonElement = when (value) { is SnapshotState.Root -> output.json.encodeToJsonElement(RootState.serializer(), value.value) is SnapshotState.Session -> output.json.encodeToJsonElement(SessionState.serializer(), value.value) + is SnapshotState.Chat -> output.json.encodeToJsonElement(ChatState.serializer(), value.value) is SnapshotState.Terminal -> output.json.encodeToJsonElement(TerminalState.serializer(), value.value) is SnapshotState.Changeset -> output.json.encodeToJsonElement(ChangesetState.serializer(), value.value) is SnapshotState.ResourceWatch -> output.json.encodeToJsonElement(ResourceWatchState.serializer(), value.value) diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt index dcd058d7..46792dac 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt @@ -13,10 +13,10 @@ import com.microsoft.agenthostprotocol.generated.ResponsePartKind import com.microsoft.agenthostprotocol.generated.ResponsePartMarkdown import com.microsoft.agenthostprotocol.generated.ResponsePartReasoning import com.microsoft.agenthostprotocol.generated.ResponsePartUnknown -import com.microsoft.agenthostprotocol.generated.SessionInputNumberQuestion -import com.microsoft.agenthostprotocol.generated.SessionInputQuestion -import com.microsoft.agenthostprotocol.generated.SessionInputQuestionNumber -import com.microsoft.agenthostprotocol.generated.SessionInputQuestionKind +import com.microsoft.agenthostprotocol.generated.ChatInputNumberQuestion +import com.microsoft.agenthostprotocol.generated.ChatInputQuestion +import com.microsoft.agenthostprotocol.generated.ChatInputQuestionNumber +import com.microsoft.agenthostprotocol.generated.ChatInputQuestionKind import com.microsoft.agenthostprotocol.generated.StringOrMarkdown import com.microsoft.agenthostprotocol.generated.ToolResultContent import kotlinx.serialization.json.JsonObject @@ -74,9 +74,9 @@ class DiscriminatedUnionTest { } @Test - fun `SessionInputQuestion accepts both number and integer wire kinds`() { + fun `ChatInputQuestion accepts both number and integer wire kinds`() { // Both "number" and "integer" wire values map to the same Kotlin - // data class (SessionInputNumberQuestion); the union serializer + // data class (ChatInputNumberQuestion); the union serializer // must dispatch on either. val numberWire = """{ "kind": "number", @@ -89,23 +89,23 @@ class DiscriminatedUnionTest { "message": "Pick an integer" }""".trimIndent() - val asNumber = json.decodeFromString(SessionInputQuestion.serializer(), numberWire) - val asInteger = json.decodeFromString(SessionInputQuestion.serializer(), integerWire) + val asNumber = json.decodeFromString(ChatInputQuestion.serializer(), numberWire) + val asInteger = json.decodeFromString(ChatInputQuestion.serializer(), integerWire) - val numberVariant = assertIs(asNumber) - val integerVariant = assertIs(asInteger) + val numberVariant = assertIs(asNumber) + val integerVariant = assertIs(asInteger) assertEquals("q1", numberVariant.value.id) assertEquals("q2", integerVariant.value.id) - assertEquals(SessionInputQuestionKind.NUMBER, numberVariant.value.kind) - assertEquals(SessionInputQuestionKind.INTEGER, integerVariant.value.kind) + assertEquals(ChatInputQuestionKind.NUMBER, numberVariant.value.kind) + assertEquals(ChatInputQuestionKind.INTEGER, integerVariant.value.kind) // Encode preserves whichever discriminator was originally set on // the data class. val reEncodedInteger = json.encodeToString( - SessionInputQuestion.serializer(), - SessionInputQuestionNumber( - SessionInputNumberQuestion( - kind = SessionInputQuestionKind.INTEGER, + ChatInputQuestion.serializer(), + ChatInputQuestionNumber( + ChatInputNumberQuestion( + kind = ChatInputQuestionKind.INTEGER, id = "q3", message = "Yet another integer", ), diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt index 229a69c0..37cf486b 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt @@ -1,5 +1,6 @@ package com.microsoft.agenthostprotocol +import com.microsoft.agenthostprotocol.generated.ChatState import com.microsoft.agenthostprotocol.generated.ChangesetState import com.microsoft.agenthostprotocol.generated.AnnotationsState import com.microsoft.agenthostprotocol.generated.ResourceWatchState @@ -51,9 +52,8 @@ class FixtureDrivenReducerTest { @BeforeEach fun mockTimestamp() { - // Match the TypeScript test mock (`Date.now = () => 9999`) so any - // fixture that asserts a `modifiedAt: 9999` field aligns with our - // reducer-produced output. + // Match the TypeScript test mock (`Date.now = () => 9999`) so chat + // fixtures assert the corresponding ISO `modifiedAt` value. originalProvider = currentTimestampProvider currentTimestampProvider = { MOCK_NOW } } @@ -156,6 +156,18 @@ class FixtureDrivenReducerTest { }, ) + "chat" -> compareFixture( + file = file, + initial = initial, + expected = expected, + serializer = ChatState.serializer(), + run = { state -> + var s = state + for (action in actions) s = chatReducer(s, action) + s + }, + ) + "terminal" -> compareFixture( file = file, initial = initial, diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt index c8bbc154..92d93481 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt @@ -1,46 +1,6 @@ package com.microsoft.agenthostprotocol -import com.microsoft.agenthostprotocol.generated.AgentInfo -import com.microsoft.agenthostprotocol.generated.ChangesetFile -import com.microsoft.agenthostprotocol.generated.ChangesetState -import com.microsoft.agenthostprotocol.generated.ChangesetStatus -import com.microsoft.agenthostprotocol.generated.ChangesetStatusChangedAction -import com.microsoft.agenthostprotocol.generated.ActionType -import com.microsoft.agenthostprotocol.generated.ChangesetClearedAction -import com.microsoft.agenthostprotocol.generated.ChangesetFileSetAction -import com.microsoft.agenthostprotocol.generated.CustomizationUnknown -import com.microsoft.agenthostprotocol.generated.ErrorInfo -import com.microsoft.agenthostprotocol.generated.FileEdit -import com.microsoft.agenthostprotocol.generated.Message -import com.microsoft.agenthostprotocol.generated.PendingMessage -import com.microsoft.agenthostprotocol.generated.PendingMessageKind -import com.microsoft.agenthostprotocol.generated.RootAgentsChangedAction -import com.microsoft.agenthostprotocol.generated.RootState -import com.microsoft.agenthostprotocol.generated.SessionCustomizationUpdatedAction -import com.microsoft.agenthostprotocol.generated.SessionLifecycle -import com.microsoft.agenthostprotocol.generated.SessionPendingMessageSetAction -import com.microsoft.agenthostprotocol.generated.SessionQueuedMessagesReorderedAction -import com.microsoft.agenthostprotocol.generated.SessionState -import com.microsoft.agenthostprotocol.generated.SessionStatus -import com.microsoft.agenthostprotocol.generated.SessionSummary -import com.microsoft.agenthostprotocol.generated.SessionTitleChangedAction -import com.microsoft.agenthostprotocol.generated.StateActionChangesetCleared -import com.microsoft.agenthostprotocol.generated.StateActionChangesetFileSet -import com.microsoft.agenthostprotocol.generated.StateActionChangesetStatusChanged -import com.microsoft.agenthostprotocol.generated.StateActionRootAgentsChanged -import com.microsoft.agenthostprotocol.generated.StateActionSessionCustomizationUpdated -import com.microsoft.agenthostprotocol.generated.StateActionSessionPendingMessageSet -import com.microsoft.agenthostprotocol.generated.StateActionSessionQueuedMessagesReordered -import com.microsoft.agenthostprotocol.generated.StateActionSessionTitleChanged -import com.microsoft.agenthostprotocol.generated.StateActionTerminalData -import com.microsoft.agenthostprotocol.generated.StateActionTerminalInput -import com.microsoft.agenthostprotocol.generated.TerminalClientClaim -import com.microsoft.agenthostprotocol.generated.TerminalClaimClient -import com.microsoft.agenthostprotocol.generated.TerminalClaimKind -import com.microsoft.agenthostprotocol.generated.TerminalContentPartUnclassified -import com.microsoft.agenthostprotocol.generated.TerminalDataAction -import com.microsoft.agenthostprotocol.generated.TerminalInputAction -import com.microsoft.agenthostprotocol.generated.TerminalState +import com.microsoft.agenthostprotocol.generated.* import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject @@ -173,14 +133,14 @@ class ReducersTest { PendingMessage(id = "m2", message = userMessage("2")), PendingMessage(id = "m3", message = userMessage("3")), ) - val session = newSession().copy(queuedMessages = original) - val reorder = StateActionSessionQueuedMessagesReordered( - SessionQueuedMessagesReorderedAction( - type = ActionType.SESSION_QUEUED_MESSAGES_REORDERED, + val chat = newChat().copy(queuedMessages = original) + val reorder = StateActionChatQueuedMessagesReordered( + ChatQueuedMessagesReorderedAction( + type = ActionType.CHAT_QUEUED_MESSAGES_REORDERED, order = listOf("m3", "m1"), ), ) - val result = sessionReducer(session, reorder) + val result = chatReducer(chat, reorder) assertEquals(listOf("m3", "m1", "m2"), result.queuedMessages?.map { it.id }) } @@ -190,61 +150,61 @@ class ReducersTest { PendingMessage(id = "m1", message = userMessage("1")), PendingMessage(id = "m2", message = userMessage("2")), ) - val session = newSession().copy(queuedMessages = original) - val reorder = StateActionSessionQueuedMessagesReordered( - SessionQueuedMessagesReorderedAction( - type = ActionType.SESSION_QUEUED_MESSAGES_REORDERED, + val chat = newChat().copy(queuedMessages = original) + val reorder = StateActionChatQueuedMessagesReordered( + ChatQueuedMessagesReorderedAction( + type = ActionType.CHAT_QUEUED_MESSAGES_REORDERED, order = listOf("m2", "m999", "m2", "m1"), ), ) - val result = sessionReducer(session, reorder) + val result = chatReducer(chat, reorder) assertEquals(listOf("m2", "m1"), result.queuedMessages?.map { it.id }) } @Test fun `pendingMessageSet upserts steering and queued messages distinctly`() { - val session = newSession() - val setSteering = StateActionSessionPendingMessageSet( - SessionPendingMessageSetAction( - type = ActionType.SESSION_PENDING_MESSAGE_SET, + val chat = newChat() + val setSteering = StateActionChatPendingMessageSet( + ChatPendingMessageSetAction( + type = ActionType.CHAT_PENDING_MESSAGE_SET, kind = PendingMessageKind.STEERING, id = "s1", message = userMessage("steer"), ), ) - val withSteering = sessionReducer(session, setSteering) + val withSteering = chatReducer(chat, setSteering) assertEquals("s1", withSteering.steeringMessage?.id) assertNull(withSteering.queuedMessages) - val setQueued1 = StateActionSessionPendingMessageSet( - SessionPendingMessageSetAction( - type = ActionType.SESSION_PENDING_MESSAGE_SET, + val setQueued1 = StateActionChatPendingMessageSet( + ChatPendingMessageSetAction( + type = ActionType.CHAT_PENDING_MESSAGE_SET, kind = PendingMessageKind.QUEUED, id = "q1", message = userMessage("q-1"), ), ) - val setQueued2 = StateActionSessionPendingMessageSet( - SessionPendingMessageSetAction( - type = ActionType.SESSION_PENDING_MESSAGE_SET, + val setQueued2 = StateActionChatPendingMessageSet( + ChatPendingMessageSetAction( + type = ActionType.CHAT_PENDING_MESSAGE_SET, kind = PendingMessageKind.QUEUED, id = "q2", message = userMessage("q-2"), ), ) - val withTwo = sessionReducer(sessionReducer(withSteering, setQueued1), setQueued2) + val withTwo = chatReducer(chatReducer(withSteering, setQueued1), setQueued2) assertEquals(listOf("q1", "q2"), withTwo.queuedMessages?.map { it.id }) // Re-setting q1 with a new body should replace in place rather than append. - val replaceQueued1 = StateActionSessionPendingMessageSet( - SessionPendingMessageSetAction( - type = ActionType.SESSION_PENDING_MESSAGE_SET, + val replaceQueued1 = StateActionChatPendingMessageSet( + ChatPendingMessageSetAction( + type = ActionType.CHAT_PENDING_MESSAGE_SET, kind = PendingMessageKind.QUEUED, id = "q1", message = userMessage("q-1-revised"), ), ) - val withReplacement = sessionReducer(withTwo, replaceQueued1) + val withReplacement = chatReducer(withTwo, replaceQueued1) assertEquals(listOf("q1", "q2"), withReplacement.queuedMessages?.map { it.id }) assertEquals("q-1-revised", withReplacement.queuedMessages?.first()?.message?.text) } @@ -258,6 +218,18 @@ class ReducersTest { ) val result = sessionReducer(session, titleAction) assertEquals(12345L, result.summary.modifiedAt) + + val chatResult = chatReducer( + newChat(), + StateActionChatTurnStarted( + ChatTurnStartedAction( + type = ActionType.CHAT_TURN_STARTED, + turnId = "turn-1", + message = userMessage("hello"), + ), + ), + ) + assertEquals("1970-01-01T00:00:12.345Z", chatResult.modifiedAt) } @Test @@ -322,7 +294,7 @@ class ReducersTest { private fun newSession(): SessionState = SessionState( summary = SessionSummary( - resource = "copilot:/test", + resource = "ahp-session:/test", provider = "copilot", title = "Test", status = SessionStatus.IDLE, @@ -330,6 +302,14 @@ class ReducersTest { modifiedAt = 1000L, ), lifecycle = SessionLifecycle.READY, + chats = emptyList(), + ) + + private fun newChat(): ChatState = ChatState( + resource = "ahp-chat:/test/default", + title = "Test", + status = SessionStatus.IDLE, + modifiedAt = "1970-01-01T00:00:01Z", turns = emptyList(), ) diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 120512d0..734f1086 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -36,12 +36,22 @@ matching `## [X.Y.Z]` heading is missing from this file. resending its entries. Handled by the annotations reducer (no-op on unknown id). -### Added - +- `ahp-chat:` channel for per-chat conversation state; `SessionState.chats[]` catalog; `SessionState.defaultChat?` input-routing hint; `ChatOrigin` provenance union; `createChat` / `disposeChat` commands. +- `ChatSummary.working_directory` — optional per-chat working directory. Falls back to the session's `working_directory` when absent. +- Three discrete chat-catalog actions on the session channel — `SessionChatAdded` (upsert by `summary.resource`), `SessionChatRemoved`, and `SessionChatUpdated` (partial-update payload). - `RootState` now exposes an optional `_meta` property bag (`meta: Option`) for implementation-defined agent-host metadata, such as a well-known `hostBuild` key carrying the host's build version/commit/date. +### Changed + +- `ChatState` is now flat — the previous embedded `summary` has been replaced with inlined `resource` / `title` / `status` / `activity` / `modified_at` / `model` / `agent` / `origin` / `working_directory` fields. `ChatSummary` remains as the standalone catalog entry on `SessionState.chats`. +- `ChatSummary.modified_at` and `ChatState.modified_at` are now ISO 8601 `String` values instead of `u64` milliseconds. + +### Removed + +- `SessionChatsChanged` variant on `StateAction` (replaced by the three discrete chat-catalog variants above). + ## [0.3.0] — 2026-06-05 Implements AHP 0.3.0. @@ -87,11 +97,14 @@ Implements AHP 0.3.0. ### Changed +- `fetchTurns` and `completions` now target an `ahp-chat:` channel; `PROTOCOL_VERSION` bumped to `0.4.0`. +- Reducers split into per-chat and session-aggregate handlers to match the multi-chat protocol shape. `SessionInput*` types renamed to `ChatInput*` (they now live on the chat channel). - Renamed the `ChangesetSummary` type to `Changeset`. The on-the-wire shape is unchanged. - Moved the `changesets` catalogue from `SessionSummary` to `SessionState`. The `session/changesetsChanged` action now updates `state.changesets` directly instead of `state.summary.changesets`. ### Removed +- `SessionState.turns`, `SessionState.activeTurn`, `SessionState.steeringMessage`, `SessionState.queuedMessages`, `SessionState.inputRequests` (moved to `ChatState`). - Removed the `additions`, `deletions`, and `files` fields from `ChangesetSummary`. Aggregate counts now live on `SessionSummary.changes`; per-changeset views derive their own totals from `ChangesetState.files`. ### Changed diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 6e34003d..9d61e65b 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -13,12 +13,12 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::state::{ AgentInfo, AgentSelection, Annotation, AnnotationEntry, Changeset, ChangesetFile, - ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ConfirmationOption, + ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer, + ChatInputRequest, ChatInputResponseKind, ChatOrigin, ChatSummary, ConfirmationOption, Customization, ErrorInfo, McpServerState, Message, ModelSelection, PendingMessageKind, - ResponsePart, SessionActiveClient, SessionInputAnswer, SessionInputRequest, - SessionInputResponseKind, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, - ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolDefinition, - ToolResultContent, UsageInfo, + ResponsePart, SessionActiveClient, TerminalClaim, TerminalInfo, TextRange, + ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, + ToolDefinition, ToolResultContent, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -34,38 +34,46 @@ pub enum ActionType { SessionReady, #[serde(rename = "session/creationFailed")] SessionCreationFailed, - #[serde(rename = "session/turnStarted")] - SessionTurnStarted, - #[serde(rename = "session/delta")] - SessionDelta, - #[serde(rename = "session/responsePart")] - SessionResponsePart, - #[serde(rename = "session/toolCallStart")] - SessionToolCallStart, - #[serde(rename = "session/toolCallDelta")] - SessionToolCallDelta, - #[serde(rename = "session/toolCallReady")] - SessionToolCallReady, - #[serde(rename = "session/toolCallConfirmed")] - SessionToolCallConfirmed, - #[serde(rename = "session/toolCallComplete")] - SessionToolCallComplete, - #[serde(rename = "session/toolCallResultConfirmed")] - SessionToolCallResultConfirmed, - #[serde(rename = "session/toolCallContentChanged")] - SessionToolCallContentChanged, - #[serde(rename = "session/turnComplete")] - SessionTurnComplete, - #[serde(rename = "session/turnCancelled")] - SessionTurnCancelled, - #[serde(rename = "session/error")] - SessionError, + #[serde(rename = "session/chatAdded")] + SessionChatAdded, + #[serde(rename = "session/chatRemoved")] + SessionChatRemoved, + #[serde(rename = "session/chatUpdated")] + SessionChatUpdated, + #[serde(rename = "session/defaultChatChanged")] + SessionDefaultChatChanged, + #[serde(rename = "chat/turnStarted")] + ChatTurnStarted, + #[serde(rename = "chat/delta")] + ChatDelta, + #[serde(rename = "chat/responsePart")] + ChatResponsePart, + #[serde(rename = "chat/toolCallStart")] + ChatToolCallStart, + #[serde(rename = "chat/toolCallDelta")] + ChatToolCallDelta, + #[serde(rename = "chat/toolCallReady")] + ChatToolCallReady, + #[serde(rename = "chat/toolCallConfirmed")] + ChatToolCallConfirmed, + #[serde(rename = "chat/toolCallComplete")] + ChatToolCallComplete, + #[serde(rename = "chat/toolCallResultConfirmed")] + ChatToolCallResultConfirmed, + #[serde(rename = "chat/toolCallContentChanged")] + ChatToolCallContentChanged, + #[serde(rename = "chat/turnComplete")] + ChatTurnComplete, + #[serde(rename = "chat/turnCancelled")] + ChatTurnCancelled, + #[serde(rename = "chat/error")] + ChatError, #[serde(rename = "session/titleChanged")] SessionTitleChanged, - #[serde(rename = "session/usage")] - SessionUsage, - #[serde(rename = "session/reasoning")] - SessionReasoning, + #[serde(rename = "chat/usage")] + ChatUsage, + #[serde(rename = "chat/reasoning")] + ChatReasoning, #[serde(rename = "session/modelChanged")] SessionModelChanged, #[serde(rename = "session/agentChanged")] @@ -76,18 +84,18 @@ pub enum ActionType { SessionActiveClientChanged, #[serde(rename = "session/activeClientToolsChanged")] SessionActiveClientToolsChanged, - #[serde(rename = "session/pendingMessageSet")] - SessionPendingMessageSet, - #[serde(rename = "session/pendingMessageRemoved")] - SessionPendingMessageRemoved, - #[serde(rename = "session/queuedMessagesReordered")] - SessionQueuedMessagesReordered, - #[serde(rename = "session/inputRequested")] - SessionInputRequested, - #[serde(rename = "session/inputAnswerChanged")] - SessionInputAnswerChanged, - #[serde(rename = "session/inputCompleted")] - SessionInputCompleted, + #[serde(rename = "chat/pendingMessageSet")] + ChatPendingMessageSet, + #[serde(rename = "chat/pendingMessageRemoved")] + ChatPendingMessageRemoved, + #[serde(rename = "chat/queuedMessagesReordered")] + ChatQueuedMessagesReordered, + #[serde(rename = "chat/inputRequested")] + ChatInputRequested, + #[serde(rename = "chat/inputAnswerChanged")] + ChatInputAnswerChanged, + #[serde(rename = "chat/inputCompleted")] + ChatInputCompleted, #[serde(rename = "session/customizationsChanged")] SessionCustomizationsChanged, #[serde(rename = "session/customizationToggled")] @@ -98,8 +106,8 @@ pub enum ActionType { SessionCustomizationRemoved, #[serde(rename = "session/mcpServerStateChanged")] SessionMcpServerStateChanged, - #[serde(rename = "session/truncated")] - SessionTruncated, + #[serde(rename = "chat/truncated")] + ChatTruncated, #[serde(rename = "session/isReadChanged")] SessionIsReadChanged, #[serde(rename = "session/isArchivedChanged")] @@ -238,12 +246,63 @@ pub struct SessionCreationFailedAction { pub error: ErrorInfo, } +/// A chat was added to this session's catalog. Upsert semantics: if a chat +/// with the same `summary.resource` already exists, the existing entry is +/// replaced. +/// +/// Mirrors the root-channel `root/sessionAdded` notification. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionChatAddedAction { + /// The full summary of the newly added (or upserted) chat. + pub summary: ChatSummary, +} + +/// A chat was removed from this session's catalog. No-op when no entry matches. +/// +/// Mirrors the root-channel `root/sessionRemoved` notification. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionChatRemovedAction { + /// The URI of the chat to remove. + pub chat: Uri, +} + +/// One existing chat's summary fields changed. +/// +/// Partial-update semantics: only fields present in `changes` are written; +/// omitted fields are preserved. Identity fields (`resource`) MUST NOT be +/// carried in `changes`. No-op when no entry with `chat` exists — clients +/// SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. +/// +/// Mirrors the root-channel `root/sessionSummaryChanged` notification. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionChatUpdatedAction { + /// The URI of the chat whose summary changed. + pub chat: Uri, + /// Mutable summary fields that changed; omitted fields are unchanged. + /// + /// Identity fields (`resource`) never change and MUST be omitted by + /// senders; receivers SHOULD ignore them if present. + pub changes: PartialChatSummary, +} + +/// The default chat input-routing hint for this session changed. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct SessionDefaultChatChangedAction { + /// New default chat URI, or `undefined` to clear the hint. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_chat: Option, +} + /// A new message has been sent to the agent, and a new turn starts. /// /// A client is only allowed to send {@link MessageKind.User} messages. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionTurnStartedAction { +pub struct ChatTurnStartedAction { /// Turn identifier pub turn_id: String, /// The new message @@ -255,11 +314,11 @@ pub struct SessionTurnStartedAction { /// Streaming text chunk from the assistant, appended to a specific response part. /// -/// The server MUST first emit a `session/responsePart` to create the target +/// The server MUST first emit a `chat/responsePart` to create the target /// part (markdown or reasoning), then use this action to append text to it. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionDeltaAction { +pub struct ChatDeltaAction { /// Turn identifier pub turn_id: String, /// Identifier of the response part to append to @@ -271,7 +330,7 @@ pub struct SessionDeltaAction { /// Structured content appended to the response. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionResponsePartAction { +pub struct ChatResponsePartAction { /// Turn identifier pub turn_id: String, /// Response part (markdown or content ref) @@ -283,11 +342,11 @@ pub struct SessionResponsePartAction { /// The server sets {@link ToolCallContributor | `contributor`} to identify /// the origin of the tool. For client-provided tools, the named client is /// responsible for executing the tool once it reaches the `running` state -/// and dispatching `session/toolCallComplete`. For MCP-served tools, the +/// and dispatching `chat/toolCallComplete`. For MCP-served tools, the /// server executes the call against the named `McpServerCustomization`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionToolCallStartAction { +pub struct ChatToolCallStartAction { /// Turn identifier pub turn_id: String, /// Tool call identifier @@ -313,7 +372,7 @@ pub struct SessionToolCallStartAction { /// Streaming partial parameters for a tool call. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionToolCallDeltaAction { +pub struct ChatToolCallDeltaAction { /// Turn identifier pub turn_id: String, /// Tool call identifier @@ -341,14 +400,14 @@ pub struct SessionToolCallDeltaAction { /// When dispatched for a `running` tool call (e.g. mid-execution permission needed), /// transitions back to `pending-confirmation`. The `invocationMessage` and `_meta` /// SHOULD be updated to describe the specific confirmation needed. Clients use the -/// standard `session/toolCallConfirmed` flow to approve or deny. +/// standard `chat/toolCallConfirmed` flow to approve or deny. /// /// For client-provided tools, the server typically sets `confirmed` to /// `'not-needed'` so the tool transitions directly to `running`, where the /// owning client can begin execution immediately. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionToolCallReadyAction { +pub struct ChatToolCallReadyAction { /// Turn identifier pub turn_id: String, /// Tool call identifier @@ -389,7 +448,7 @@ pub struct SessionToolCallReadyAction { /// Client approves or denies a pending tool call (merged approved + denied variants). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionToolCallConfirmedAction { +pub struct ChatToolCallConfirmedAction { pub turn_id: String, pub tool_call_id: String, /// Additional provider-specific metadata for this tool call. @@ -429,7 +488,7 @@ pub struct SessionToolCallConfirmedAction { /// this action with `result.success = false` and an appropriate error. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionToolCallCompleteAction { +pub struct ChatToolCallCompleteAction { /// Turn identifier pub turn_id: String, /// Tool call identifier @@ -454,7 +513,7 @@ pub struct SessionToolCallCompleteAction { /// If `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionToolCallResultConfirmedAction { +pub struct ChatToolCallResultConfirmedAction { /// Turn identifier pub turn_id: String, /// Tool call identifier @@ -471,10 +530,39 @@ pub struct SessionToolCallResultConfirmedAction { pub approved: bool, } +/// Partial content produced while a tool is still executing. +/// +/// Replaces the `content` array on the running tool call state. Clients can +/// use this to display live feedback (e.g. a terminal reference) before the +/// tool completes. +/// +/// For client-provided tools (where `toolClientId` is set on the tool call state), +/// the owning client dispatches this action to stream intermediate content while +/// executing. The server SHOULD reject this action if the dispatching client does +/// not match `toolClientId`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatToolCallContentChangedAction { + /// Turn identifier + pub turn_id: String, + /// Tool call identifier + pub tool_call_id: String, + /// Additional provider-specific metadata for this tool call. + /// + /// Clients MAY look for well-known keys here to provide enhanced UI. + /// For example, a `ptyTerminal` key with `{ input: string; output: string }` + /// indicates the tool operated on a terminal (both `input` and `output` may + /// contain escape sequences). + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// The current partial content for the running tool call + pub content: Vec, +} + /// Turn finished — the assistant is idle. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionTurnCompleteAction { +pub struct ChatTurnCompleteAction { /// Turn identifier pub turn_id: String, } @@ -482,7 +570,7 @@ pub struct SessionTurnCompleteAction { /// Turn was aborted; server stops processing. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionTurnCancelledAction { +pub struct ChatTurnCancelledAction { /// Turn identifier pub turn_id: String, } @@ -490,7 +578,7 @@ pub struct SessionTurnCancelledAction { /// Error during turn processing. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionErrorAction { +pub struct ChatErrorAction { /// Turn identifier pub turn_id: String, /// Error details @@ -509,7 +597,7 @@ pub struct SessionTitleChangedAction { /// Token usage report for a turn. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionUsageAction { +pub struct ChatUsageAction { /// Turn identifier pub turn_id: String, /// Token usage data @@ -518,11 +606,11 @@ pub struct SessionUsageAction { /// Reasoning/thinking text from the model, appended to a specific reasoning response part. /// -/// The server MUST first emit a `session/responsePart` to create the target +/// The server MUST first emit a `chat/responsePart` to create the target /// reasoning part, then use this action to append text to it. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionReasoningAction { +pub struct ChatReasoningAction { /// Turn identifier pub turn_id: String, /// Identifier of the reasoning response part to append to @@ -648,14 +736,14 @@ pub struct SessionActiveClientToolsChangedAction { /// /// For steering messages, this always replaces the single steering message. /// For queued messages, if a message with the given `id` already exists it is -/// updated in place; otherwise it is appended to the queue. If the session is +/// updated in place; otherwise it is appended to the queue. If the chat is /// idle when a queued message is set, the server SHOULD immediately consume it /// and start a new turn. /// /// A client is only allowed to send {@link MessageKind.User} messages. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionPendingMessageSetAction { +pub struct ChatPendingMessageSetAction { /// Whether this is a steering or queued message pub kind: PendingMessageKind, /// Unique identifier for this pending message @@ -671,7 +759,7 @@ pub struct SessionPendingMessageSetAction { /// injecting a steering message into the current turn). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionPendingMessageRemovedAction { +pub struct ChatPendingMessageRemovedAction { /// Whether this is a steering or queued message pub kind: PendingMessageKind, /// Identifier of the pending message to remove @@ -687,7 +775,7 @@ pub struct SessionPendingMessageRemovedAction { /// view of the queue never silently drops messages). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionQueuedMessagesReorderedAction { +pub struct ChatQueuedMessagesReorderedAction { /// Queued message IDs in the desired order pub order: Vec, } @@ -699,9 +787,9 @@ pub struct SessionQueuedMessagesReorderedAction { /// unless `request.answers` is provided. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputRequestedAction { +pub struct ChatInputRequestedAction { /// Input request to create or replace - pub request: SessionInputRequest, + pub request: ChatInputRequest, } /// A client updated, submitted, skipped, or removed a single in-progress answer. @@ -709,14 +797,14 @@ pub struct SessionInputRequestedAction { /// Dispatching with `answer: undefined` removes that question's answer draft. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputAnswerChangedAction { +pub struct ChatInputAnswerChangedAction { /// Input request identifier pub request_id: String, /// Question identifier within the input request pub question_id: String, /// Updated answer, or `undefined` to clear an answer draft #[serde(default, skip_serializing_if = "Option::is_none")] - pub answer: Option, + pub answer: Option, } /// A client accepted, declined, or cancelled a session input request. @@ -725,14 +813,14 @@ pub struct SessionInputAnswerChangedAction { /// synced answer state to resume the blocked operation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputCompletedAction { +pub struct ChatInputCompletedAction { /// Input request identifier pub request_id: String, /// Completion outcome - pub response: SessionInputResponseKind, + pub response: ChatInputResponseKind, /// Optional final answer replacement, keyed by question ID #[serde(default, skip_serializing_if = "Option::is_none")] - pub answers: Option>, + pub answers: Option>, } /// The session's customizations have changed. @@ -826,14 +914,14 @@ pub struct SessionMcpServerStateChangedAction { /// turn are removed and the specified turn is kept. If `turnId` is omitted, all /// turns are removed. /// -/// If there is an active turn it is silently dropped and the session status +/// If there is an active turn it is silently dropped and the chat status /// returns to `idle`. /// /// Common use-case: truncate old data then dispatch a new -/// `session/turnStarted` with an edited message. +/// `chat/turnStarted` with an edited message. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub struct SessionTruncatedAction { +pub struct ChatTruncatedAction { /// Keep turns up to and including this turn. Omit to clear all turns. #[serde(default, skip_serializing_if = "Option::is_none")] pub turn_id: Option, @@ -865,35 +953,6 @@ pub struct SessionMetaChangedAction { pub meta: Option, } -/// Partial content produced while a tool is still executing. -/// -/// Replaces the `content` array on the running tool call state. Clients can -/// use this to display live feedback (e.g. a terminal reference) before the -/// tool completes. -/// -/// For client-provided tools (where `toolClientId` is set on the tool call state), -/// the owning client dispatches this action to stream intermediate content while -/// executing. The server SHOULD reject this action if the dispatching client does -/// not match `toolClientId`. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionToolCallContentChangedAction { - /// Turn identifier - pub turn_id: String, - /// Tool call identifier - pub tool_call_id: String, - /// Additional provider-specific metadata for this tool call. - /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The current partial content for the running tool call - pub content: Vec, -} - /// The {@link ChangesetState.status} for this changeset transitioned (e.g. /// `computing → ready`). The error payload is set together with `status` /// whenever it transitions to {@link ChangesetStatus.Error | Error}. @@ -1240,6 +1299,45 @@ pub struct ResourceWatchChangedAction { pub changes: AnyValue, } +// ─── Partial Summaries ──────────────────────────────────────────────── + +/// Partial equivalent of ChatSummary — every field is optional for delta updates. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct PartialChatSummary { + /// Chat URI + #[serde(default, skip_serializing_if = "Option::is_none")] + pub resource: Option, + /// Chat title + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Current chat status (reuses SessionStatus shape) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, + /// Human-readable description of what the chat is currently doing + #[serde(default, skip_serializing_if = "Option::is_none")] + pub activity: Option, + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub modified_at: Option, + /// Optional per-chat model override (defaults to the session's model) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + /// Optional per-chat agent override (defaults to the session's agent) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option, + /// How this chat came into existence + #[serde(default, skip_serializing_if = "Option::is_none")] + pub origin: Option, + /// Optional per-chat working directory. + /// + /// If absent, the chat inherits + /// {@link SessionSummary.workingDirectory | the session's working directory}. + /// See {@link ChatState.workingDirectory} for usage notes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directory: Option, +} + // ─── StateAction Union ─────────────────────────────────────────────── /// Discriminated union of every state action. @@ -1256,36 +1354,46 @@ pub enum StateAction { SessionReady(SessionReadyAction), #[serde(rename = "session/creationFailed")] SessionCreationFailed(SessionCreationFailedAction), - #[serde(rename = "session/turnStarted")] - SessionTurnStarted(SessionTurnStartedAction), - #[serde(rename = "session/delta")] - SessionDelta(SessionDeltaAction), - #[serde(rename = "session/responsePart")] - SessionResponsePart(SessionResponsePartAction), - #[serde(rename = "session/toolCallStart")] - SessionToolCallStart(SessionToolCallStartAction), - #[serde(rename = "session/toolCallDelta")] - SessionToolCallDelta(SessionToolCallDeltaAction), - #[serde(rename = "session/toolCallReady")] - SessionToolCallReady(SessionToolCallReadyAction), - #[serde(rename = "session/toolCallConfirmed")] - SessionToolCallConfirmed(SessionToolCallConfirmedAction), - #[serde(rename = "session/toolCallComplete")] - SessionToolCallComplete(SessionToolCallCompleteAction), - #[serde(rename = "session/toolCallResultConfirmed")] - SessionToolCallResultConfirmed(SessionToolCallResultConfirmedAction), - #[serde(rename = "session/turnComplete")] - SessionTurnComplete(SessionTurnCompleteAction), - #[serde(rename = "session/turnCancelled")] - SessionTurnCancelled(SessionTurnCancelledAction), - #[serde(rename = "session/error")] - SessionError(SessionErrorAction), + #[serde(rename = "session/chatAdded")] + SessionChatAdded(SessionChatAddedAction), + #[serde(rename = "session/chatRemoved")] + SessionChatRemoved(SessionChatRemovedAction), + #[serde(rename = "session/chatUpdated")] + SessionChatUpdated(SessionChatUpdatedAction), + #[serde(rename = "session/defaultChatChanged")] + SessionDefaultChatChanged(SessionDefaultChatChangedAction), + #[serde(rename = "chat/turnStarted")] + ChatTurnStarted(ChatTurnStartedAction), + #[serde(rename = "chat/delta")] + ChatDelta(ChatDeltaAction), + #[serde(rename = "chat/responsePart")] + ChatResponsePart(ChatResponsePartAction), + #[serde(rename = "chat/toolCallStart")] + ChatToolCallStart(ChatToolCallStartAction), + #[serde(rename = "chat/toolCallDelta")] + ChatToolCallDelta(ChatToolCallDeltaAction), + #[serde(rename = "chat/toolCallReady")] + ChatToolCallReady(ChatToolCallReadyAction), + #[serde(rename = "chat/toolCallConfirmed")] + ChatToolCallConfirmed(ChatToolCallConfirmedAction), + #[serde(rename = "chat/toolCallComplete")] + ChatToolCallComplete(ChatToolCallCompleteAction), + #[serde(rename = "chat/toolCallResultConfirmed")] + ChatToolCallResultConfirmed(ChatToolCallResultConfirmedAction), + #[serde(rename = "chat/toolCallContentChanged")] + ChatToolCallContentChanged(ChatToolCallContentChangedAction), + #[serde(rename = "chat/turnComplete")] + ChatTurnComplete(ChatTurnCompleteAction), + #[serde(rename = "chat/turnCancelled")] + ChatTurnCancelled(ChatTurnCancelledAction), + #[serde(rename = "chat/error")] + ChatError(ChatErrorAction), #[serde(rename = "session/titleChanged")] SessionTitleChanged(SessionTitleChangedAction), - #[serde(rename = "session/usage")] - SessionUsage(SessionUsageAction), - #[serde(rename = "session/reasoning")] - SessionReasoning(SessionReasoningAction), + #[serde(rename = "chat/usage")] + ChatUsage(ChatUsageAction), + #[serde(rename = "chat/reasoning")] + ChatReasoning(ChatReasoningAction), #[serde(rename = "session/modelChanged")] SessionModelChanged(SessionModelChangedAction), #[serde(rename = "session/agentChanged")] @@ -1304,18 +1412,18 @@ pub enum StateAction { SessionActiveClientChanged(SessionActiveClientChangedAction), #[serde(rename = "session/activeClientToolsChanged")] SessionActiveClientToolsChanged(SessionActiveClientToolsChangedAction), - #[serde(rename = "session/pendingMessageSet")] - SessionPendingMessageSet(SessionPendingMessageSetAction), - #[serde(rename = "session/pendingMessageRemoved")] - SessionPendingMessageRemoved(SessionPendingMessageRemovedAction), - #[serde(rename = "session/queuedMessagesReordered")] - SessionQueuedMessagesReordered(SessionQueuedMessagesReorderedAction), - #[serde(rename = "session/inputRequested")] - SessionInputRequested(SessionInputRequestedAction), - #[serde(rename = "session/inputAnswerChanged")] - SessionInputAnswerChanged(SessionInputAnswerChangedAction), - #[serde(rename = "session/inputCompleted")] - SessionInputCompleted(SessionInputCompletedAction), + #[serde(rename = "chat/pendingMessageSet")] + ChatPendingMessageSet(ChatPendingMessageSetAction), + #[serde(rename = "chat/pendingMessageRemoved")] + ChatPendingMessageRemoved(ChatPendingMessageRemovedAction), + #[serde(rename = "chat/queuedMessagesReordered")] + ChatQueuedMessagesReordered(ChatQueuedMessagesReorderedAction), + #[serde(rename = "chat/inputRequested")] + ChatInputRequested(ChatInputRequestedAction), + #[serde(rename = "chat/inputAnswerChanged")] + ChatInputAnswerChanged(ChatInputAnswerChangedAction), + #[serde(rename = "chat/inputCompleted")] + ChatInputCompleted(ChatInputCompletedAction), #[serde(rename = "session/customizationsChanged")] SessionCustomizationsChanged(SessionCustomizationsChangedAction), #[serde(rename = "session/customizationToggled")] @@ -1326,14 +1434,12 @@ pub enum StateAction { SessionCustomizationRemoved(SessionCustomizationRemovedAction), #[serde(rename = "session/mcpServerStateChanged")] SessionMcpServerStateChanged(Box), - #[serde(rename = "session/truncated")] - SessionTruncated(SessionTruncatedAction), + #[serde(rename = "chat/truncated")] + ChatTruncated(ChatTruncatedAction), #[serde(rename = "session/configChanged")] SessionConfigChanged(SessionConfigChangedAction), #[serde(rename = "session/metaChanged")] SessionMetaChanged(SessionMetaChangedAction), - #[serde(rename = "session/toolCallContentChanged")] - SessionToolCallContentChanged(SessionToolCallContentChangedAction), #[serde(rename = "changeset/statusChanged")] ChangesetStatusChanged(ChangesetStatusChangedAction), #[serde(rename = "changeset/fileSet")] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index a87e1626..53bde24f 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -15,7 +15,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::actions::{ActionEnvelope, StateAction}; #[allow(unused_imports)] use crate::state::{ - AgentSelection, ContentRef, MessageAttachment, ModelSelection, SessionActiveClient, + AgentSelection, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn, }; @@ -312,6 +312,46 @@ pub struct DisposeSessionParams { pub channel: Uri, } +/// Identifies a source chat and turn to fork from. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatForkSource { + /// URI of the existing chat to fork from + pub chat: Uri, + /// Turn ID in the source chat; content up to and including this turn's response is copied + pub turn_id: String, +} + +/// Creates a new chat within a session. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateChatParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Chat URI (client-chosen, e.g. `ahp-chat:/`). + pub chat: Uri, + /// Optional initial message for the new chat. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub initial_message: Option, + /// Optional per-chat model override. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + /// Optional per-chat agent override. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option, + /// Optional source chat and turn to fork from. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +/// Disposes a chat and cleans up server-side resources. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DisposeChatParams { + /// Channel URI this command targets. + pub channel: Uri, +} + /// Returns a list of session summaries. Used to populate session lists and sidebars. /// /// The session list is **not** part of the state tree because it can be arbitrarily @@ -730,7 +770,7 @@ pub struct CreateResourceWatchResult { pub channel: Uri, } -/// Fetches historical turns for a session. Used for lazy loading of conversation +/// Fetches historical turns for a chat. Used for lazy loading of conversation /// history. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 826f3bef..5cc5f2d9 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -214,7 +214,10 @@ pub struct PartialSessionSummary { /// — the session uses the provider's default behavior. #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option, - /// The working directory URI for this session + /// The default working directory URI for this session. Individual chats + /// MAY override via {@link ChatSummary.workingDirectory | their own + /// `workingDirectory`}; this field acts as the fallback for any chat that + /// does not. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, /// Aggregate summary of file changes associated with this session. Servers diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 5e8677e7..87e46501 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -68,9 +68,19 @@ pub enum SessionStatus { IsArchived = 64, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ChatOriginKind { + #[serde(rename = "user")] + User, + #[serde(rename = "fork")] + Fork, + #[serde(rename = "tool")] + Tool, +} + /// Answer lifecycle state. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SessionInputAnswerState { +pub enum ChatInputAnswerState { #[serde(rename = "draft")] Draft, #[serde(rename = "submitted")] @@ -81,7 +91,7 @@ pub enum SessionInputAnswerState { /// Answer value kind. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SessionInputAnswerValueKind { +pub enum ChatInputAnswerValueKind { #[serde(rename = "text")] Text, #[serde(rename = "number")] @@ -96,7 +106,7 @@ pub enum SessionInputAnswerValueKind { /// Question/input control kind. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SessionInputQuestionKind { +pub enum ChatInputQuestionKind { #[serde(rename = "text")] Text, #[serde(rename = "number")] @@ -113,7 +123,7 @@ pub enum SessionInputQuestionKind { /// How a client completed an input request. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SessionInputResponseKind { +pub enum ChatInputResponseKind { #[serde(rename = "accept")] Accept, #[serde(rename = "decline")] @@ -744,6 +754,103 @@ pub struct PendingMessage { pub message: Message, } +/// Full state for a single chat, loaded when a client subscribes to the chat's +/// URI. +/// +/// The lightweight catalog representation of a chat is {@link ChatSummary}, +/// carried in {@link SessionState.chats | `SessionState.chats`}. `ChatState` +/// **denormalizes** every {@link ChatSummary} field directly onto itself so +/// subscribers receive one flat object instead of having to merge a nested +/// `summary` sub-object. Producers MUST keep the two representations +/// consistent: any change to the inlined fields below SHOULD also be +/// announced on the parent session via the matching +/// {@link SessionChatUpdatedAction | `session/chatUpdated`} action. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatState { + /// Chat URI + pub resource: Uri, + /// Chat title + pub title: String, + /// Current chat status (reuses SessionStatus shape) + pub status: u32, + /// Human-readable description of what the chat is currently doing + #[serde(default, skip_serializing_if = "Option::is_none")] + pub activity: Option, + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + pub modified_at: String, + /// Optional per-chat model override (defaults to the session's model) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + /// Optional per-chat agent override (defaults to the session's agent) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option, + /// How this chat came into existence + #[serde(default, skip_serializing_if = "Option::is_none")] + pub origin: Option, + /// Optional per-chat working directory. + /// + /// If absent, the chat inherits + /// {@link SessionSummary.workingDirectory | the session's working directory}. + /// Hosts MAY override this for individual chats — for example, to give a + /// subordinate chat its own git worktree so multiple chats in a session can + /// make independent edits that the orchestrator later merges back. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directory: Option, + /// Completed turns + pub turns: Vec, + /// Currently in-progress turn + #[serde(default, skip_serializing_if = "Option::is_none")] + pub active_turn: Option, + /// Message to inject into the current turn at a convenient point + #[serde(default, skip_serializing_if = "Option::is_none")] + pub steering_message: Option, + /// Messages to send automatically as new turns after the current turn finishes + #[serde(default, skip_serializing_if = "Option::is_none")] + pub queued_messages: Option>, + /// Requests for user input that are currently blocking or informing chat progress + #[serde(default, skip_serializing_if = "Option::is_none")] + pub input_requests: Option>, + /// Additional provider-specific metadata for this chat. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +/// Lightweight catalog entry for a chat, carried in +/// {@link SessionState.chats | `SessionState.chats`}. The full conversation +/// lives in {@link ChatState}, which inlines (denormalizes) every field below. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatSummary { + /// Chat URI + pub resource: Uri, + /// Chat title + pub title: String, + /// Current chat status (reuses SessionStatus shape) + pub status: u32, + /// Human-readable description of what the chat is currently doing + #[serde(default, skip_serializing_if = "Option::is_none")] + pub activity: Option, + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + pub modified_at: String, + /// Optional per-chat model override (defaults to the session's model) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + /// Optional per-chat agent override (defaults to the session's agent) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option, + /// How this chat came into existence + #[serde(default, skip_serializing_if = "Option::is_none")] + pub origin: Option, + /// Optional per-chat working directory. + /// + /// If absent, the chat inherits + /// {@link SessionSummary.workingDirectory | the session's working directory}. + /// See {@link ChatState.workingDirectory} for usage notes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directory: Option, +} + /// Full state for a single session, loaded when a client subscribes to the session's URI. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -761,20 +868,14 @@ pub struct SessionState { /// The client currently providing tools and interactive capabilities to this session #[serde(default, skip_serializing_if = "Option::is_none")] pub active_client: Option, - /// Completed turns - pub turns: Vec, - /// Currently in-progress turn - #[serde(default, skip_serializing_if = "Option::is_none")] - pub active_turn: Option, - /// Message to inject into the current turn at a convenient point - #[serde(default, skip_serializing_if = "Option::is_none")] - pub steering_message: Option, - /// Messages to send automatically as new turns after the current turn finishes - #[serde(default, skip_serializing_if = "Option::is_none")] - pub queued_messages: Option>, - /// Requests for user input that are currently blocking or informing session progress - #[serde(default, skip_serializing_if = "Option::is_none")] - pub input_requests: Option>, + /// Catalog of chats in this session. + pub chats: Vec, + /// The chat that receives input when the user addresses the session without + /// selecting a specific chat. This is a UI routing hint, not a hierarchy + /// marker — chats remain equal peers at the protocol level. Hosts MAY change + /// this over the session's lifetime. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_chat: Option, /// Session configuration schema and current values #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, @@ -839,6 +940,39 @@ pub struct SessionActiveClient { pub customizations: Option>, } +/// Lightweight catalog entry summarizing one session. Surfaced via +/// {@link RootChannelCommands.listSessions | `root/listSessions`} and +/// `root/sessionAdded`/`root/sessionSummaryChanged` notifications. +/// +/// **Aggregation across chats.** Once a session contains more than one chat, +/// several `SessionSummary` fields are derived from the underlying +/// {@link SessionState.chats | chat catalog}. Producers SHOULD follow these +/// rules so clients that only consume the session summary (e.g. a session +/// list) still see meaningful state: +/// +/// - `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` / +/// `Error` — bits 0–4) from the +/// {@link SessionState.defaultChat | default chat} when present, else from +/// the most recently modified chat. **Promote** `InputNeeded` whenever any +/// chat in the session needs input, and **promote** `Error` whenever any +/// chat is in an error state — both override the default-chat bits. The +/// orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped. +/// - `activity`: mirror the activity string of the default chat, or of the +/// chat currently driving the promoted status bits when a non-default chat +/// wins (e.g. the chat that raised `InputNeeded`). +/// - `modifiedAt`: the max of all chats' `modifiedAt`. +/// - `model` / `agent`: the session-level selection. Per-chat overrides are +/// surfaced on individual {@link ChatSummary} entries, not aggregated up. +/// - `workingDirectory`: the session-level **default**. Individual chats MAY +/// override via {@link ChatSummary.workingDirectory}; aggregating these up +/// is meaningless and SHOULD NOT be attempted. +/// - `changes`: optional roll-up across all chats. Producers MAY sum the +/// per-chat changeset stats or report the most expensive chat's stats — +/// whichever is cheaper for the host to compute. +/// +/// Sessions with a single chat trivially satisfy all of the above (the chat's +/// values pass through unchanged). The rules only matter once a session +/// carries multiple chats. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionSummary { @@ -869,7 +1003,10 @@ pub struct SessionSummary { /// — the session uses the provider's default behavior. #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option, - /// The working directory URI for this session + /// The default working directory URI for this session. Individual chats + /// MAY override via {@link ChatSummary.workingDirectory | their own + /// `workingDirectory`}; this field acts as the fallback for any chat that + /// does not. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, /// Aggregate summary of file changes associated with this session. Servers @@ -1058,7 +1195,7 @@ pub struct Message { /// A choice in a select-style question. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputOption { +pub struct ChatInputOption { /// Stable option identifier; for MCP enum values this is the enum string pub id: String, /// Display label @@ -1074,25 +1211,25 @@ pub struct SessionInputOption { /// Value captured for one answer. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputTextAnswerValue { +pub struct ChatInputTextAnswerValue { pub value: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputNumberAnswerValue { +pub struct ChatInputNumberAnswerValue { pub value: f64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputBooleanAnswerValue { +pub struct ChatInputBooleanAnswerValue { pub value: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputSelectedAnswerValue { +pub struct ChatInputSelectedAnswerValue { pub value: String, /// Free-form text entered instead of selecting an option #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1101,7 +1238,7 @@ pub struct SessionInputSelectedAnswerValue { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputSelectedManyAnswerValue { +pub struct ChatInputSelectedManyAnswerValue { pub value: Vec, /// Free-form text entered in addition to selected options #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1110,23 +1247,23 @@ pub struct SessionInputSelectedManyAnswerValue { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputAnswered { +pub struct ChatInputAnswered { /// Answer value - pub value: SessionInputAnswerValue, + pub value: ChatInputAnswerValue, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] -pub struct SessionInputSkipped { +pub struct ChatInputSkipped { /// Free-form reason or value captured while skipping, if any #[serde(default, skip_serializing_if = "Option::is_none")] pub freeform_values: Option>, } -/// Text question within a session input request. +/// Text question within a chat input request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputTextQuestion { +pub struct ChatInputTextQuestion { /// Stable question identifier used as the key in `answers` pub id: String, /// Short display title @@ -1151,10 +1288,10 @@ pub struct SessionInputTextQuestion { pub default_value: Option, } -/// Numeric question within a session input request. +/// Numeric question within a chat input request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputNumberQuestion { +pub struct ChatInputNumberQuestion { /// Stable question identifier used as the key in `answers` pub id: String, /// Short display title @@ -1176,10 +1313,10 @@ pub struct SessionInputNumberQuestion { pub default_value: Option, } -/// Boolean question within a session input request. +/// Boolean question within a chat input request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputBooleanQuestion { +pub struct ChatInputBooleanQuestion { /// Stable question identifier used as the key in `answers` pub id: String, /// Short display title @@ -1195,10 +1332,10 @@ pub struct SessionInputBooleanQuestion { pub default_value: Option, } -/// Single-select question within a session input request. +/// Single-select question within a chat input request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputSingleSelectQuestion { +pub struct ChatInputSingleSelectQuestion { /// Stable question identifier used as the key in `answers` pub id: String, /// Short display title @@ -1210,16 +1347,16 @@ pub struct SessionInputSingleSelectQuestion { #[serde(default, skip_serializing_if = "Option::is_none")] pub required: Option, /// Options the user may select from - pub options: Vec, + pub options: Vec, /// Whether the user may enter text instead of selecting an option #[serde(default, skip_serializing_if = "Option::is_none")] pub allow_freeform_input: Option, } -/// Multi-select question within a session input request. +/// Multi-select question within a chat input request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputMultiSelectQuestion { +pub struct ChatInputMultiSelectQuestion { /// Stable question identifier used as the key in `answers` pub id: String, /// Short display title @@ -1231,7 +1368,7 @@ pub struct SessionInputMultiSelectQuestion { #[serde(default, skip_serializing_if = "Option::is_none")] pub required: Option, /// Options the user may select from - pub options: Vec, + pub options: Vec, /// Whether the user may enter text in addition to selecting options #[serde(default, skip_serializing_if = "Option::is_none")] pub allow_freeform_input: Option, @@ -1245,12 +1382,12 @@ pub struct SessionInputMultiSelectQuestion { /// A live request for user input. /// -/// The server creates or replaces requests with `session/inputRequested`. -/// Clients sync drafts with `session/inputAnswerChanged` and complete requests -/// with `session/inputCompleted`. +/// The server creates or replaces requests with `chat/inputRequested`. +/// Clients sync drafts with `chat/inputAnswerChanged` and complete requests +/// with `chat/inputCompleted`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionInputRequest { +pub struct ChatInputRequest { /// Stable request identifier pub id: String, /// Display message for the request as a whole @@ -1261,10 +1398,10 @@ pub struct SessionInputRequest { pub url: Option, /// Ordered questions to ask the user #[serde(default, skip_serializing_if = "Option::is_none")] - pub questions: Option>, + pub questions: Option>, /// Current draft or submitted answers, keyed by question ID #[serde(default, skip_serializing_if = "Option::is_none")] - pub answers: Option>, + pub answers: Option>, } /// A zero-based position within a textual document. @@ -1481,7 +1618,7 @@ pub struct MessageAnnotationsAttachment { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MarkdownResponsePart { - /// Part identifier, used by `session/delta` to target this part for content appends + /// Part identifier, used by `chat/delta` to target this part for content appends pub id: String, /// Markdown content pub content: String, @@ -1531,7 +1668,7 @@ pub struct ToolCallResponsePart { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReasoningResponsePart { - /// Part identifier, used by `session/reasoning` to target this part for content appends + /// Part identifier, used by `chat/reasoning` to target this part for content appends pub id: String, /// Accumulated reasoning text pub content: String, @@ -2564,7 +2701,7 @@ pub struct ToolCallClientContributor { /// Absent for server-side tools. /// /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// dispatching `chat/toolCallComplete` with the result. pub client_id: String, } @@ -2742,7 +2879,7 @@ pub struct ErrorInfo { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Snapshot { - /// The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`) + /// The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`) pub resource: Uri, /// The current state of the resource pub state: SnapshotState, @@ -3066,6 +3203,37 @@ pub struct ResourceChange { // ─── Discriminated Unions ───────────────────────────────────────────── +/// How a chat came into existence. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum ChatOrigin { + /// Created directly by a user. + #[serde(rename = "user")] + User, + /// Forked from a specific turn of another chat. + #[serde(rename = "fork")] + Fork { + /// URI of the chat this one was forked from. + chat: Uri, + /// Turn the fork was taken from. + #[serde(rename = "turnId")] + turn_id: String, + }, + /// Spawned by a tool call in another chat. + #[serde(rename = "tool")] + Tool { + /// URI of the chat whose tool call spawned this one. + chat: Uri, + /// Tool call that spawned this chat. + #[serde(rename = "toolCallId")] + tool_call_id: String, + }, + /// 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), +} + /// A single part of a response stream (text, tool call, reasoning, content reference). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] @@ -3136,22 +3304,22 @@ pub enum TerminalContentPart { Unknown(serde_json::Value), } -/// One question within a session input request. +/// One question within a chat input request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] -pub enum SessionInputQuestion { +pub enum ChatInputQuestion { #[serde(rename = "text")] - Text(SessionInputTextQuestion), + Text(ChatInputTextQuestion), #[serde(rename = "number")] - Number(SessionInputNumberQuestion), + Number(ChatInputNumberQuestion), #[serde(rename = "integer")] - Integer(SessionInputNumberQuestion), + Integer(ChatInputNumberQuestion), #[serde(rename = "boolean")] - Boolean(SessionInputBooleanQuestion), + Boolean(ChatInputBooleanQuestion), #[serde(rename = "single-select")] - SingleSelect(SessionInputSingleSelectQuestion), + SingleSelect(ChatInputSingleSelectQuestion), #[serde(rename = "multi-select")] - MultiSelect(SessionInputMultiSelectQuestion), + MultiSelect(ChatInputMultiSelectQuestion), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] @@ -3161,17 +3329,17 @@ pub enum SessionInputQuestion { /// Value captured for one answer. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] -pub enum SessionInputAnswerValue { +pub enum ChatInputAnswerValue { #[serde(rename = "text")] - Text(SessionInputTextAnswerValue), + Text(ChatInputTextAnswerValue), #[serde(rename = "number")] - Number(SessionInputNumberAnswerValue), + Number(ChatInputNumberAnswerValue), #[serde(rename = "boolean")] - Boolean(SessionInputBooleanAnswerValue), + Boolean(ChatInputBooleanAnswerValue), #[serde(rename = "selected")] - Selected(SessionInputSelectedAnswerValue), + Selected(ChatInputSelectedAnswerValue), #[serde(rename = "selected-many")] - SelectedMany(SessionInputSelectedManyAnswerValue), + SelectedMany(ChatInputSelectedManyAnswerValue), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] @@ -3181,13 +3349,13 @@ pub enum SessionInputAnswerValue { /// Draft, submitted, or skipped answer for one question. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "state")] -pub enum SessionInputAnswer { +pub enum ChatInputAnswer { #[serde(rename = "draft")] - Draft(SessionInputAnswered), + Draft(ChatInputAnswered), #[serde(rename = "submitted")] - Submitted(SessionInputAnswered), + Submitted(ChatInputAnswered), #[serde(rename = "skipped")] - Skipped(SessionInputSkipped), + Skipped(ChatInputSkipped), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] @@ -3324,17 +3492,19 @@ pub enum ToolCallContributor { Unknown(serde_json::Value), } -/// The state payload of a snapshot — root, session, terminal, +/// The state payload of a snapshot — root, session, chat, terminal, /// changeset, resource-watch, or annotations state. /// /// Deserialized by trying session first (has required `summary`), then -/// terminal (has required `content`), then changeset (has required -/// `status` and `files`), then resource-watch (has required `root` and -/// `recursive`), then annotations (has required `annotations`), then root. +/// chat (has required `turns`), then terminal (has required `content`), +/// then changeset (has required `status` and `files`), then resource-watch +/// (has required `root` and `recursive`), then annotations (has required +/// `annotations`), then root. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum SnapshotState { Session(Box), + Chat(Box), Terminal(Box), Changeset(Box), ResourceWatch(Box), diff --git a/clients/rust/crates/ahp/src/multi_host_state_mirror.rs b/clients/rust/crates/ahp/src/multi_host_state_mirror.rs index b224c41f..7288894b 100644 --- a/clients/rust/crates/ahp/src/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/src/multi_host_state_mirror.rs @@ -37,12 +37,14 @@ use std::collections::HashMap; use ahp_types::actions::ActionEnvelope; use ahp_types::common::ROOT_RESOURCE_URI; use ahp_types::state::{ - AnnotationsState, ChangesetState, ResourceWatchState, RootState, SessionState, SnapshotState, - TerminalState, + AnnotationsState, ChangesetState, ChatState, ResourceWatchState, RootState, SessionState, + SnapshotState, TerminalState, }; use crate::hosts::{HostId, HostSubscriptionEvent}; -use crate::reducers::{apply_action_to_root, apply_action_to_session, apply_action_to_terminal}; +use crate::reducers::{ + apply_action_to_chat, apply_action_to_root, apply_action_to_session, apply_action_to_terminal, +}; use crate::SubscriptionEvent; /// Compound key tagging a channel URI with the host that produced it. @@ -85,6 +87,7 @@ impl HostedResourceKey { pub struct MultiHostStateMirror { root_states: HashMap, sessions: HashMap, + chats: HashMap, terminals: HashMap, changesets: HashMap, annotations: HashMap, @@ -107,6 +110,11 @@ impl MultiHostStateMirror { &self.sessions } + /// Borrow the chat states map keyed by `(host_id, uri)`. + pub fn chats(&self) -> &HashMap { + &self.chats + } + /// Borrow the terminal states map keyed by `(host_id, uri)`. pub fn terminals(&self) -> &HashMap { &self.terminals @@ -162,6 +170,10 @@ impl MultiHostStateMirror { apply_action_to_session(session, &envelope.action); return; } + if let Some(chat) = self.chats.get_mut(&key) { + apply_action_to_chat(chat, &envelope.action); + return; + } if let Some(terminal) = self.terminals.get_mut(&key) { apply_action_to_terminal(terminal, &envelope.action); } @@ -184,6 +196,9 @@ impl MultiHostStateMirror { SnapshotState::Session(state) => { self.sessions.insert(key, state.as_ref().clone()); } + SnapshotState::Chat(state) => { + self.chats.insert(key, state.as_ref().clone()); + } SnapshotState::Terminal(state) => { self.terminals.insert(key, state.as_ref().clone()); } @@ -204,6 +219,7 @@ impl MultiHostStateMirror { pub fn reset_host(&mut self, host: &HostId) { self.root_states.remove(host); self.sessions.retain(|key, _| &key.host_id != host); + self.chats.retain(|key, _| &key.host_id != host); self.terminals.retain(|key, _| &key.host_id != host); self.changesets.retain(|key, _| &key.host_id != host); self.annotations.retain(|key, _| &key.host_id != host); @@ -214,6 +230,7 @@ impl MultiHostStateMirror { pub fn reset(&mut self) { self.root_states.clear(); self.sessions.clear(); + self.chats.clear(); self.terminals.clear(); self.changesets.clear(); self.annotations.clear(); diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index b7d57853..df3e279f 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -1,13 +1,13 @@ //! Pure state reducers ported from `types/reducers.ts`. //! //! Reducers mutate state in place and return a [`ReduceOutcome`]. Use -//! [`apply_action_to_root`], [`apply_action_to_session`], and -//! [`apply_action_to_terminal`] to dispatch any [`StateAction`] against -//! the matching scope; unrelated actions short-circuit as -//! [`ReduceOutcome::OutOfScope`] so a client holding all three state -//! trees can blindly fan every action out. +//! [`apply_action_to_root`], [`apply_action_to_session`], +//! [`apply_action_to_chat`], and [`apply_action_to_terminal`] to +//! dispatch any [`StateAction`] against the matching scope; unrelated +//! actions short-circuit as [`ReduceOutcome::OutOfScope`] so a client +//! holding every state tree can blindly fan each action out. //! -//! All three reducers are pure functions over `(state, action)` — no +//! All reducers are pure functions over `(state, action)` — no //! I/O, no allocation beyond what the action itself carries — which //! makes them safe to run inside a UI render loop or a snapshot //! reconciler. @@ -50,13 +50,13 @@ use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; use ahp_types::actions::{ - SessionInputAnswerChangedAction, SessionToolCallCompleteAction, SessionToolCallConfirmedAction, - SessionToolCallContentChangedAction, SessionToolCallDeltaAction, SessionToolCallReadyAction, - SessionToolCallResultConfirmedAction, SessionTurnStartedAction, StateAction, + ChatInputAnswerChangedAction, ChatToolCallCompleteAction, ChatToolCallConfirmedAction, + ChatToolCallContentChangedAction, ChatToolCallDeltaAction, ChatToolCallReadyAction, + ChatToolCallResultConfirmedAction, ChatTurnStartedAction, StateAction, }; use ahp_types::state::{ - ActiveTurn, ChildCustomization, ConfirmationOption, Customization, ErrorInfo, PendingMessage, - PendingMessageKind, ResponsePart, RootState, SessionInputRequest, SessionLifecycle, + ActiveTurn, ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, + ErrorInfo, PendingMessage, PendingMessageKind, ResponsePart, RootState, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, TerminalContentPart, TerminalState, TerminalUnclassifiedPart, ToolCallCancellationReason, ToolCallCancelledState, ToolCallCompletedState, ToolCallConfirmationReason, ToolCallContributor, @@ -95,6 +95,33 @@ fn now_ms() -> i64 { .unwrap_or(0) } +fn now_iso() -> String { + iso8601_from_unix_millis(now_ms()) +} + +fn iso8601_from_unix_millis(ms: i64) -> String { + let seconds = ms.div_euclid(1_000); + let millis = ms.rem_euclid(1_000); + let days = seconds.div_euclid(86_400); + let seconds_of_day = seconds.rem_euclid(86_400); + let hour = seconds_of_day / 3_600; + let minute = (seconds_of_day % 3_600) / 60; + let second = seconds_of_day % 60; + + let z = days + 719_468; + let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097); + let doe = z - era * 146_097; + let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let day = doy - (153 * mp + 2) / 5 + 1; + let month = mp + if mp < 10 { 3 } else { -9 }; + let year = y + if month <= 2 { 1 } else { 0 }; + + format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z") +} + fn tool_call_meta( tc: &ToolCallState, ) -> ( @@ -163,7 +190,7 @@ fn tool_call_id(tc: &ToolCallState) -> &str { } } -fn has_pending_tool_call_confirmation(state: &SessionState) -> bool { +fn has_pending_tool_call_confirmation(state: &ChatState) -> bool { let Some(active) = &state.active_turn else { return false; }; @@ -189,7 +216,7 @@ fn with_status_flag(status: u32, flag: SessionStatus, set: bool) -> u32 { } } -fn summary_status(state: &SessionState, terminal: Option) -> u32 { +fn summary_status(state: &ChatState, terminal: Option) -> u32 { let activity: u32 = if let Some(t) = terminal { t as u32 } else if state @@ -205,19 +232,23 @@ fn summary_status(state: &SessionState, terminal: Option) -> u32 } else { SessionStatus::Idle as u32 }; - (state.summary.status & !STATUS_ACTIVITY_MASK) | activity + (state.status & !STATUS_ACTIVITY_MASK) | activity } -fn refresh_summary_status(state: &mut SessionState) { - state.summary.status = summary_status(state, None); +fn refresh_summary_status(state: &mut ChatState) { + state.status = summary_status(state, None); } -fn touch_modified(state: &mut SessionState) { +fn touch_chat_modified(state: &mut ChatState) { + state.modified_at = now_iso(); +} + +fn touch_session_modified(state: &mut SessionState) { state.summary.modified_at = now_ms(); } fn end_turn( - state: &mut SessionState, + state: &mut ChatState, turn_id: &str, turn_state: TurnState, terminal_status: Option, @@ -296,12 +327,12 @@ fn end_turn( state.turns.push(turn); state.input_requests = None; - touch_modified(state); - state.summary.status = summary_status(state, terminal_status); + touch_chat_modified(state); + state.status = summary_status(state, terminal_status); ReduceOutcome::Applied } -fn upsert_input_request(state: &mut SessionState, request: SessionInputRequest) { +fn upsert_input_request(state: &mut ChatState, request: ChatInputRequest) { let existing = state.input_requests.get_or_insert_with(Vec::new); if let Some(idx) = existing.iter().position(|r| r.id == request.id) { let answers = request @@ -314,9 +345,9 @@ fn upsert_input_request(state: &mut SessionState, request: SessionInputRequest) } else { existing.push(request); } - state.summary.status = summary_status(state, None); - touch_modified(state); - state.summary.status = with_status_flag(state.summary.status, SessionStatus::IsRead, false); + state.status = summary_status(state, None); + touch_chat_modified(state); + state.status = with_status_flag(state.status, SessionStatus::IsRead, false); } // ─── Customization Helpers ─────────────────────────────────────────────────── @@ -368,7 +399,7 @@ fn apply_toggle(list: &mut [Customization], id: &str, enabled: bool) -> bool { } fn update_tool_call( - state: &mut SessionState, + state: &mut ChatState, turn_id: &str, tool_call_id_target: &str, updater: F, @@ -410,7 +441,7 @@ where } fn update_response_part( - state: &mut SessionState, + state: &mut ChatState, turn_id: &str, part_id: &str, updater: F, @@ -491,117 +522,75 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - state.creation_error = Some(a.error.clone()); ReduceOutcome::Applied } - StateAction::SessionTurnStarted(a) => apply_turn_started(state, a), - StateAction::SessionDelta(a) => update_response_part(state, &a.turn_id, &a.part_id, |p| { - if let ResponsePart::Markdown(m) = p { - m.content.push_str(&a.content); + StateAction::SessionChatAdded(a) => { + if let Some(idx) = state + .chats + .iter() + .position(|chat| chat.resource == a.summary.resource) + { + state.chats[idx] = a.summary.clone(); + } else { + state.chats.push(a.summary.clone()); } - }), - StateAction::SessionResponsePart(a) => { - let Some(active) = state.active_turn.as_mut() else { + ReduceOutcome::Applied + } + StateAction::SessionChatRemoved(a) => { + let Some(idx) = state.chats.iter().position(|chat| chat.resource == a.chat) else { return ReduceOutcome::NoOp; }; - if active.id != a.turn_id { - return ReduceOutcome::NoOp; + state.chats.remove(idx); + if state.default_chat.as_ref() == Some(&a.chat) { + state.default_chat = None; } - active.response_parts.push(a.part.clone()); ReduceOutcome::Applied } - StateAction::SessionTurnComplete(a) => { - end_turn(state, &a.turn_id, TurnState::Complete, None, None) - } - StateAction::SessionTurnCancelled(a) => { - end_turn(state, &a.turn_id, TurnState::Cancelled, None, None) - } - StateAction::SessionError(a) => end_turn( - state, - &a.turn_id, - TurnState::Error, - Some(SessionStatus::Error), - Some(a.error.clone()), - ), - StateAction::SessionToolCallStart(a) => { - let Some(active) = state.active_turn.as_mut() else { + StateAction::SessionChatUpdated(a) => { + let Some(chat) = state.chats.iter_mut().find(|chat| chat.resource == a.chat) else { return ReduceOutcome::NoOp; }; - if active.id != a.turn_id { - return ReduceOutcome::NoOp; + if let Some(title) = &a.changes.title { + chat.title = title.clone(); } - active - .response_parts - .push(ResponsePart::ToolCall(Box::new(ToolCallResponsePart { - tool_call: ToolCallState::Streaming(ToolCallStreamingState { - tool_call_id: a.tool_call_id.clone(), - tool_name: a.tool_name.clone(), - display_name: a.display_name.clone(), - contributor: a.contributor.clone(), - meta: a.meta.clone(), - partial_input: None, - invocation_message: None, - }), - }))); - ReduceOutcome::Applied - } - StateAction::SessionToolCallDelta(a) => apply_tool_call_delta(state, a), - StateAction::SessionToolCallReady(a) => { - let res = apply_tool_call_ready(state, a); - if res == ReduceOutcome::Applied { - refresh_summary_status(state); + if let Some(status) = a.changes.status { + chat.status = status; } - res - } - StateAction::SessionToolCallConfirmed(a) => { - let res = apply_tool_call_confirmed(state, a); - if res == ReduceOutcome::Applied { - refresh_summary_status(state); + if let Some(activity) = &a.changes.activity { + chat.activity = Some(activity.clone()); } - res - } - StateAction::SessionToolCallComplete(a) => { - let res = apply_tool_call_complete(state, a); - if res == ReduceOutcome::Applied { - refresh_summary_status(state); + if let Some(modified_at) = &a.changes.modified_at { + chat.modified_at = modified_at.clone(); } - res - } - StateAction::SessionToolCallResultConfirmed(a) => { - let res = apply_tool_call_result_confirmed(state, a); - if res == ReduceOutcome::Applied { - refresh_summary_status(state); + if let Some(model) = &a.changes.model { + chat.model = Some(model.clone()); + } + if let Some(agent) = &a.changes.agent { + chat.agent = Some(agent.clone()); + } + if let Some(origin) = &a.changes.origin { + chat.origin = Some(origin.clone()); + } + if let Some(working_directory) = &a.changes.working_directory { + chat.working_directory = Some(working_directory.clone()); } - res - } - StateAction::SessionToolCallContentChanged(a) => apply_tool_call_content_changed(state, a), - StateAction::SessionTitleChanged(a) => { - state.summary.title = a.title.clone(); - touch_modified(state); ReduceOutcome::Applied } - StateAction::SessionUsage(a) => { - let Some(active) = state.active_turn.as_mut() else { - return ReduceOutcome::NoOp; - }; - if active.id != a.turn_id { - return ReduceOutcome::NoOp; - } - active.usage = Some(a.usage.clone()); + StateAction::SessionDefaultChatChanged(a) => { + state.default_chat = a.default_chat.clone(); ReduceOutcome::Applied } - StateAction::SessionReasoning(a) => { - update_response_part(state, &a.turn_id, &a.part_id, |p| { - if let ResponsePart::Reasoning(r) = p { - r.content.push_str(&a.content); - } - }) + StateAction::SessionTitleChanged(a) => { + state.summary.title = a.title.clone(); + touch_session_modified(state); + ReduceOutcome::Applied } StateAction::SessionModelChanged(a) => { state.summary.model = Some(a.model.clone()); - touch_modified(state); + touch_session_modified(state); ReduceOutcome::Applied } StateAction::SessionAgentChanged(a) => { state.summary.agent = a.agent.clone(); - touch_modified(state); + touch_session_modified(state); ReduceOutcome::Applied } StateAction::SessionIsReadChanged(a) => { @@ -636,7 +625,7 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - config.values.insert(k.clone(), v.clone()); } } - touch_modified(state); + touch_session_modified(state); ReduceOutcome::Applied } StateAction::SessionMetaChanged(a) => { @@ -751,13 +740,122 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - } ReduceOutcome::NoOp } - StateAction::SessionTruncated(a) => apply_truncated(state, a.turn_id.as_deref()), - StateAction::SessionInputRequested(a) => { + _ => ReduceOutcome::OutOfScope, + } +} + +// ─── Chat Reducer ───────────────────────────────────────────────────── + +/// Apply a [`StateAction`] to a [`ChatState`] in place. +/// +/// Handles all chat-scoped actions — turn lifecycle, tool calls, input +/// requests, and pending/queued messages. Actions targeting a different +/// scope short-circuit as [`ReduceOutcome::OutOfScope`]. +pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> ReduceOutcome { + match action { + StateAction::ChatTurnStarted(a) => apply_turn_started(state, a), + StateAction::ChatDelta(a) => update_response_part(state, &a.turn_id, &a.part_id, |p| { + if let ResponsePart::Markdown(m) = p { + m.content.push_str(&a.content); + } + }), + StateAction::ChatResponsePart(a) => { + let Some(active) = state.active_turn.as_mut() else { + return ReduceOutcome::NoOp; + }; + if active.id != a.turn_id { + return ReduceOutcome::NoOp; + } + active.response_parts.push(a.part.clone()); + ReduceOutcome::Applied + } + StateAction::ChatTurnComplete(a) => { + end_turn(state, &a.turn_id, TurnState::Complete, None, None) + } + StateAction::ChatTurnCancelled(a) => { + end_turn(state, &a.turn_id, TurnState::Cancelled, None, None) + } + StateAction::ChatError(a) => end_turn( + state, + &a.turn_id, + TurnState::Error, + Some(SessionStatus::Error), + Some(a.error.clone()), + ), + StateAction::ChatToolCallStart(a) => { + let Some(active) = state.active_turn.as_mut() else { + return ReduceOutcome::NoOp; + }; + if active.id != a.turn_id { + return ReduceOutcome::NoOp; + } + active + .response_parts + .push(ResponsePart::ToolCall(Box::new(ToolCallResponsePart { + tool_call: ToolCallState::Streaming(ToolCallStreamingState { + tool_call_id: a.tool_call_id.clone(), + tool_name: a.tool_name.clone(), + display_name: a.display_name.clone(), + contributor: a.contributor.clone(), + meta: a.meta.clone(), + partial_input: None, + invocation_message: None, + }), + }))); + ReduceOutcome::Applied + } + StateAction::ChatToolCallDelta(a) => apply_tool_call_delta(state, a), + StateAction::ChatToolCallReady(a) => { + let res = apply_tool_call_ready(state, a); + if res == ReduceOutcome::Applied { + refresh_summary_status(state); + } + res + } + StateAction::ChatToolCallConfirmed(a) => { + let res = apply_tool_call_confirmed(state, a); + if res == ReduceOutcome::Applied { + refresh_summary_status(state); + } + res + } + StateAction::ChatToolCallComplete(a) => { + let res = apply_tool_call_complete(state, a); + if res == ReduceOutcome::Applied { + refresh_summary_status(state); + } + res + } + StateAction::ChatToolCallResultConfirmed(a) => { + let res = apply_tool_call_result_confirmed(state, a); + if res == ReduceOutcome::Applied { + refresh_summary_status(state); + } + res + } + StateAction::ChatToolCallContentChanged(a) => apply_tool_call_content_changed(state, a), + StateAction::ChatUsage(a) => { + let Some(active) = state.active_turn.as_mut() else { + return ReduceOutcome::NoOp; + }; + if active.id != a.turn_id { + return ReduceOutcome::NoOp; + } + active.usage = Some(a.usage.clone()); + ReduceOutcome::Applied + } + StateAction::ChatReasoning(a) => update_response_part(state, &a.turn_id, &a.part_id, |p| { + if let ResponsePart::Reasoning(r) = p { + r.content.push_str(&a.content); + } + }), + StateAction::ChatTruncated(a) => apply_truncated(state, a.turn_id.as_deref()), + StateAction::ChatInputRequested(a) => { upsert_input_request(state, a.request.clone()); ReduceOutcome::Applied } - StateAction::SessionInputAnswerChanged(a) => apply_input_answer_changed(state, a), - StateAction::SessionInputCompleted(a) => { + StateAction::ChatInputAnswerChanged(a) => apply_input_answer_changed(state, a), + StateAction::ChatInputCompleted(a) => { let Some(list) = state.input_requests.as_mut() else { return ReduceOutcome::NoOp; }; @@ -770,10 +868,10 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - state.input_requests = None; } refresh_summary_status(state); - touch_modified(state); + touch_chat_modified(state); ReduceOutcome::Applied } - StateAction::SessionPendingMessageSet(a) => { + StateAction::ChatPendingMessageSet(a) => { let entry = PendingMessage { id: a.id.clone(), message: a.message.clone(), @@ -793,7 +891,7 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - } ReduceOutcome::Applied } - StateAction::SessionPendingMessageRemoved(a) => match a.kind { + StateAction::ChatPendingMessageRemoved(a) => match a.kind { PendingMessageKind::Steering => match &state.steering_message { Some(m) if m.id == a.id => { state.steering_message = None; @@ -816,7 +914,7 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - ReduceOutcome::Applied } }, - StateAction::SessionQueuedMessagesReordered(a) => { + StateAction::ChatQueuedMessagesReordered(a) => { let Some(list) = state.queued_messages.as_mut() else { return ReduceOutcome::NoOp; }; @@ -842,16 +940,16 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - } } -fn apply_turn_started(state: &mut SessionState, a: &SessionTurnStartedAction) -> ReduceOutcome { +fn apply_turn_started(state: &mut ChatState, a: &ChatTurnStartedAction) -> ReduceOutcome { state.active_turn = Some(ActiveTurn { id: a.turn_id.clone(), message: a.message.clone(), response_parts: Vec::new(), usage: None, }); - state.summary.status = summary_status(state, None); - touch_modified(state); - state.summary.status = with_status_flag(state.summary.status, SessionStatus::IsRead, false); + state.status = summary_status(state, None); + touch_chat_modified(state); + state.status = with_status_flag(state.status, SessionStatus::IsRead, false); if let Some(qmid) = &a.queued_message_id { if state.steering_message.as_ref().map(|m| m.id.as_str()) == Some(qmid.as_str()) { @@ -867,10 +965,7 @@ fn apply_turn_started(state: &mut SessionState, a: &SessionTurnStartedAction) -> ReduceOutcome::Applied } -fn apply_tool_call_delta( - state: &mut SessionState, - a: &SessionToolCallDeltaAction, -) -> ReduceOutcome { +fn apply_tool_call_delta(state: &mut ChatState, a: &ChatToolCallDeltaAction) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| match tc { ToolCallState::Streaming(mut s) => { let current = s.partial_input.unwrap_or_default(); @@ -887,10 +982,7 @@ fn apply_tool_call_delta( }) } -fn apply_tool_call_ready( - state: &mut SessionState, - a: &SessionToolCallReadyAction, -) -> ReduceOutcome { +fn apply_tool_call_ready(state: &mut ChatState, a: &ChatToolCallReadyAction) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let (tool_call_id, tool_name, display_name, contributor, meta) = tool_call_meta(&tc); let meta = a.meta.clone().or(meta); @@ -940,8 +1032,8 @@ fn resolve_selected_option( } fn apply_tool_call_confirmed( - state: &mut SessionState, - a: &SessionToolCallConfirmedAction, + state: &mut ChatState, + a: &ChatToolCallConfirmedAction, ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let ToolCallState::PendingConfirmation(s) = tc else { @@ -988,8 +1080,8 @@ fn apply_tool_call_confirmed( } fn apply_tool_call_complete( - state: &mut SessionState, - a: &SessionToolCallCompleteAction, + state: &mut ChatState, + a: &ChatToolCallCompleteAction, ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let (tool_call_id, tool_name, display_name, contributor, meta) = tool_call_meta(&tc); @@ -1048,8 +1140,8 @@ fn apply_tool_call_complete( } fn apply_tool_call_result_confirmed( - state: &mut SessionState, - a: &SessionToolCallResultConfirmedAction, + state: &mut ChatState, + a: &ChatToolCallResultConfirmedAction, ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let ToolCallState::PendingResultConfirmation(s) = tc else { @@ -1091,8 +1183,8 @@ fn apply_tool_call_result_confirmed( } fn apply_tool_call_content_changed( - state: &mut SessionState, - a: &SessionToolCallContentChangedAction, + state: &mut ChatState, + a: &ChatToolCallContentChangedAction, ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| match tc { ToolCallState::Running(mut s) => { @@ -1106,7 +1198,7 @@ fn apply_tool_call_content_changed( }) } -fn apply_truncated(state: &mut SessionState, turn_id: Option<&str>) -> ReduceOutcome { +fn apply_truncated(state: &mut ChatState, turn_id: Option<&str>) -> ReduceOutcome { match turn_id { None => { state.turns.clear(); @@ -1120,14 +1212,14 @@ fn apply_truncated(state: &mut SessionState, turn_id: Option<&str>) -> ReduceOut } state.active_turn = None; state.input_requests = None; - touch_modified(state); - state.summary.status = summary_status(state, None); + touch_chat_modified(state); + state.status = summary_status(state, None); ReduceOutcome::Applied } fn apply_input_answer_changed( - state: &mut SessionState, - a: &SessionInputAnswerChangedAction, + state: &mut ChatState, + a: &ChatInputAnswerChangedAction, ) -> ReduceOutcome { let Some(list) = state.input_requests.as_mut() else { return ReduceOutcome::NoOp; @@ -1148,7 +1240,7 @@ fn apply_input_answer_changed( if answers.is_empty() { req.answers = None; } - touch_modified(state); + touch_chat_modified(state); ReduceOutcome::Applied } @@ -1241,7 +1333,7 @@ pub fn apply_action_to_terminal(state: &mut TerminalState, action: &StateAction) #[cfg(test)] mod tests { use super::*; - use ahp_types::state::{MarkdownResponsePart, Message, SessionSummary}; + use ahp_types::state::{ChatSummary, MarkdownResponsePart, Message, SessionSummary}; fn user_message(text: &str) -> Message { Message { @@ -1273,37 +1365,54 @@ mod tests { creation_error: None, server_tools: None, active_client: None, + chats: Vec::new(), + default_chat: None, + config: None, + customizations: None, + changesets: None, + meta: None, + } + } + + fn empty_chat(resource: &str) -> ChatState { + ChatState { + resource: resource.to_string(), + title: String::new(), + status: SessionStatus::Idle as u32, + activity: None, + modified_at: "1970-01-01T00:00:00.000Z".into(), + model: None, + agent: None, + origin: None, + working_directory: None, turns: Vec::new(), active_turn: None, steering_message: None, queued_messages: None, input_requests: None, - config: None, - customizations: None, - changesets: None, meta: None, } } #[test] fn turn_started_creates_active_turn_and_sets_in_progress() { - let mut s = empty_session("copilot:/s1"); - let action = StateAction::SessionTurnStarted(SessionTurnStartedAction { + let mut s = empty_chat("copilot:/s1/chat/1"); + let action = StateAction::ChatTurnStarted(ChatTurnStartedAction { turn_id: "t1".into(), message: user_message("hi"), queued_message_id: None, }); assert_eq!( - apply_action_to_session(&mut s, &action), + apply_action_to_chat(&mut s, &action), ReduceOutcome::Applied ); - assert_eq!(s.summary.status, SessionStatus::InProgress as u32); + assert_eq!(s.status, SessionStatus::InProgress as u32); assert_eq!(s.active_turn.unwrap().id, "t1"); } #[test] fn delta_appends_to_markdown_part() { - let mut s = empty_session("copilot:/s1"); + let mut s = empty_chat("copilot:/s1/chat/1"); s.active_turn = Some(ActiveTurn { id: "t1".into(), message: user_message("hi"), @@ -1313,12 +1422,12 @@ mod tests { })], usage: None, }); - let a = StateAction::SessionDelta(ahp_types::actions::SessionDeltaAction { + let a = StateAction::ChatDelta(ahp_types::actions::ChatDeltaAction { turn_id: "t1".into(), part_id: "p1".into(), content: ", world".into(), }); - assert_eq!(apply_action_to_session(&mut s, &a), ReduceOutcome::Applied); + assert_eq!(apply_action_to_chat(&mut s, &a), ReduceOutcome::Applied); match &s.active_turn.unwrap().response_parts[0] { ResponsePart::Markdown(m) => assert_eq!(m.content, "Hello, world"), _ => panic!(), @@ -1327,22 +1436,90 @@ mod tests { #[test] fn turn_complete_moves_active_to_turns_and_returns_idle() { - let mut s = empty_session("copilot:/s1"); + let mut s = empty_chat("copilot:/s1/chat/1"); s.active_turn = Some(ActiveTurn { id: "t1".into(), message: user_message("hi"), response_parts: Vec::new(), usage: None, }); - s.summary.status = SessionStatus::InProgress as u32; - let a = StateAction::SessionTurnComplete(ahp_types::actions::SessionTurnCompleteAction { + s.status = SessionStatus::InProgress as u32; + let a = StateAction::ChatTurnComplete(ahp_types::actions::ChatTurnCompleteAction { turn_id: "t1".into(), }); - assert_eq!(apply_action_to_session(&mut s, &a), ReduceOutcome::Applied); + assert_eq!(apply_action_to_chat(&mut s, &a), ReduceOutcome::Applied); assert!(s.active_turn.is_none()); assert_eq!(s.turns.len(), 1); assert_eq!(s.turns[0].state, TurnState::Complete); - assert_eq!(s.summary.status, SessionStatus::Idle as u32); + assert_eq!(s.status, SessionStatus::Idle as u32); + } + + #[test] + fn session_reducer_handles_ready_and_chat_catalog_actions() { + let mut s = empty_session("copilot:/s1"); + let ready = StateAction::SessionReady(ahp_types::actions::SessionReadyAction {}); + assert_eq!( + apply_action_to_session(&mut s, &ready), + ReduceOutcome::Applied + ); + assert_eq!(s.lifecycle, SessionLifecycle::Ready); + + let chat = ChatSummary { + resource: "copilot:/s1/chat/1".into(), + title: "c1".into(), + status: SessionStatus::Idle as u32, + activity: None, + modified_at: "1970-01-01T00:00:00.000Z".into(), + model: None, + agent: None, + origin: None, + working_directory: None, + }; + let added = StateAction::SessionChatAdded(ahp_types::actions::SessionChatAddedAction { + summary: chat.clone(), + }); + assert_eq!( + apply_action_to_session(&mut s, &added), + ReduceOutcome::Applied + ); + assert_eq!(s.chats, vec![chat.clone()]); + + let updated = + StateAction::SessionChatUpdated(ahp_types::actions::SessionChatUpdatedAction { + chat: chat.resource.clone(), + changes: ahp_types::actions::PartialChatSummary { + title: Some("renamed".into()), + modified_at: Some("1970-01-01T00:00:09.999Z".into()), + ..Default::default() + }, + }); + assert_eq!( + apply_action_to_session(&mut s, &updated), + ReduceOutcome::Applied + ); + assert_eq!(s.chats[0].title, "renamed"); + assert_eq!(s.chats[0].modified_at, "1970-01-01T00:00:09.999Z"); + + s.default_chat = Some(chat.resource.clone()); + let removed = + StateAction::SessionChatRemoved(ahp_types::actions::SessionChatRemovedAction { + chat: chat.resource.clone(), + }); + assert_eq!( + apply_action_to_session(&mut s, &removed), + ReduceOutcome::Applied + ); + assert!(s.chats.is_empty()); + assert!(s.default_chat.is_none()); + + // A chat-scoped action is out of scope for the session reducer. + let turn = StateAction::ChatTurnComplete(ahp_types::actions::ChatTurnCompleteAction { + turn_id: "t1".into(), + }); + assert_eq!( + apply_action_to_session(&mut s, &turn), + ReduceOutcome::OutOfScope + ); } #[test] @@ -1555,6 +1732,14 @@ mod tests { &file_name, description, ), + "chat" => run_fixture::( + initial, + expected, + &parsed_actions, + apply_action_to_chat, + &file_name, + description, + ), "terminal" => run_fixture::( initial, expected, 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 6b46b6b8..442597ad 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -64,11 +64,8 @@ fn session_state(title: &str, resource: &str) -> SessionState { creation_error: None, server_tools: None, active_client: None, - turns: vec![], - active_turn: None, - steering_message: None, - queued_messages: None, - input_requests: None, + chats: vec![], + default_chat: None, config: None, customizations: None, changesets: None, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index a80a4215..ded019d4 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -10,39 +10,43 @@ public enum ActionType: String, Codable, Sendable { case rootActiveSessionsChanged = "root/activeSessionsChanged" case sessionReady = "session/ready" case sessionCreationFailed = "session/creationFailed" - case sessionTurnStarted = "session/turnStarted" - case sessionDelta = "session/delta" - case sessionResponsePart = "session/responsePart" - case sessionToolCallStart = "session/toolCallStart" - case sessionToolCallDelta = "session/toolCallDelta" - case sessionToolCallReady = "session/toolCallReady" - case sessionToolCallConfirmed = "session/toolCallConfirmed" - case sessionToolCallComplete = "session/toolCallComplete" - case sessionToolCallResultConfirmed = "session/toolCallResultConfirmed" - case sessionToolCallContentChanged = "session/toolCallContentChanged" - case sessionTurnComplete = "session/turnComplete" - case sessionTurnCancelled = "session/turnCancelled" - case sessionError = "session/error" + case sessionChatAdded = "session/chatAdded" + case sessionChatRemoved = "session/chatRemoved" + case sessionChatUpdated = "session/chatUpdated" + case sessionDefaultChatChanged = "session/defaultChatChanged" + case chatTurnStarted = "chat/turnStarted" + case chatDelta = "chat/delta" + case chatResponsePart = "chat/responsePart" + case chatToolCallStart = "chat/toolCallStart" + case chatToolCallDelta = "chat/toolCallDelta" + case chatToolCallReady = "chat/toolCallReady" + case chatToolCallConfirmed = "chat/toolCallConfirmed" + case chatToolCallComplete = "chat/toolCallComplete" + case chatToolCallResultConfirmed = "chat/toolCallResultConfirmed" + case chatToolCallContentChanged = "chat/toolCallContentChanged" + case chatTurnComplete = "chat/turnComplete" + case chatTurnCancelled = "chat/turnCancelled" + case chatError = "chat/error" case sessionTitleChanged = "session/titleChanged" - case sessionUsage = "session/usage" - case sessionReasoning = "session/reasoning" + case chatUsage = "chat/usage" + case chatReasoning = "chat/reasoning" case sessionModelChanged = "session/modelChanged" case sessionAgentChanged = "session/agentChanged" case sessionServerToolsChanged = "session/serverToolsChanged" case sessionActiveClientChanged = "session/activeClientChanged" case sessionActiveClientToolsChanged = "session/activeClientToolsChanged" - case sessionPendingMessageSet = "session/pendingMessageSet" - case sessionPendingMessageRemoved = "session/pendingMessageRemoved" - case sessionQueuedMessagesReordered = "session/queuedMessagesReordered" - case sessionInputRequested = "session/inputRequested" - case sessionInputAnswerChanged = "session/inputAnswerChanged" - case sessionInputCompleted = "session/inputCompleted" + case chatPendingMessageSet = "chat/pendingMessageSet" + case chatPendingMessageRemoved = "chat/pendingMessageRemoved" + case chatQueuedMessagesReordered = "chat/queuedMessagesReordered" + case chatInputRequested = "chat/inputRequested" + case chatInputAnswerChanged = "chat/inputAnswerChanged" + case chatInputCompleted = "chat/inputCompleted" case sessionCustomizationsChanged = "session/customizationsChanged" case sessionCustomizationToggled = "session/customizationToggled" case sessionCustomizationUpdated = "session/customizationUpdated" case sessionCustomizationRemoved = "session/customizationRemoved" case sessionMcpServerStateChanged = "session/mcpServerStateChanged" - case sessionTruncated = "session/truncated" + case chatTruncated = "chat/truncated" case sessionIsReadChanged = "session/isReadChanged" case sessionIsArchivedChanged = "session/isArchivedChanged" case sessionActivityChanged = "session/activityChanged" @@ -168,7 +172,70 @@ public struct SessionCreationFailedAction: Codable, Sendable { } } -public struct SessionTurnStartedAction: Codable, Sendable { +public struct SessionChatAddedAction: Codable, Sendable { + public var type: ActionType + /// The full summary of the newly added (or upserted) chat. + public var summary: ChatSummary + + public init( + type: ActionType, + summary: ChatSummary + ) { + self.type = type + self.summary = summary + } +} + +public struct SessionChatRemovedAction: Codable, Sendable { + public var type: ActionType + /// The URI of the chat to remove. + public var chat: String + + public init( + type: ActionType, + chat: String + ) { + self.type = type + self.chat = chat + } +} + +public struct SessionChatUpdatedAction: Codable, Sendable { + public var type: ActionType + /// The URI of the chat whose summary changed. + public var chat: String + /// Mutable summary fields that changed; omitted fields are unchanged. + /// + /// Identity fields (`resource`) never change and MUST be omitted by + /// senders; receivers SHOULD ignore them if present. + public var changes: PartialChatSummary + + public init( + type: ActionType, + chat: String, + changes: PartialChatSummary + ) { + self.type = type + self.chat = chat + self.changes = changes + } +} + +public struct SessionDefaultChatChangedAction: Codable, Sendable { + public var type: ActionType + /// New default chat URI, or `undefined` to clear the hint. + public var defaultChat: String? + + public init( + type: ActionType, + defaultChat: String? = nil + ) { + self.type = type + self.defaultChat = defaultChat + } +} + +public struct ChatTurnStartedAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -190,7 +257,7 @@ public struct SessionTurnStartedAction: Codable, Sendable { } } -public struct SessionDeltaAction: Codable, Sendable { +public struct ChatDeltaAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -212,7 +279,7 @@ public struct SessionDeltaAction: Codable, Sendable { } } -public struct SessionResponsePartAction: Codable, Sendable { +public struct ChatResponsePartAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -230,7 +297,7 @@ public struct SessionResponsePartAction: Codable, Sendable { } } -public struct SessionToolCallStartAction: Codable, Sendable { +public struct ChatToolCallStartAction: Codable, Sendable { /// Turn identifier public var turnId: String /// Tool call identifier @@ -280,7 +347,7 @@ public struct SessionToolCallStartAction: Codable, Sendable { } } -public struct SessionToolCallDeltaAction: Codable, Sendable { +public struct ChatToolCallDeltaAction: Codable, Sendable { /// Turn identifier public var turnId: String /// Tool call identifier @@ -324,7 +391,7 @@ public struct SessionToolCallDeltaAction: Codable, Sendable { } } -public struct SessionToolCallReadyAction: Codable, Sendable { +public struct ChatToolCallReadyAction: Codable, Sendable { /// Turn identifier public var turnId: String /// Tool call identifier @@ -397,7 +464,7 @@ public struct SessionToolCallReadyAction: Codable, Sendable { } /// Client approves or denies a pending tool call (merged approved + denied variants). -public struct SessionToolCallConfirmedAction: Codable, Sendable { +public struct ChatToolCallConfirmedAction: Codable, Sendable { /// Action type discriminant public var type: String /// Turn identifier @@ -427,7 +494,7 @@ public struct SessionToolCallConfirmedAction: Codable, Sendable { } public init( - type: String = "session/toolCallConfirmed", + type: String = "chat/toolCallConfirmed", turnId: String, toolCallId: String, approved: Bool, @@ -453,7 +520,7 @@ public struct SessionToolCallConfirmedAction: Codable, Sendable { } } -public struct SessionToolCallCompleteAction: Codable, Sendable { +public struct ChatToolCallCompleteAction: Codable, Sendable { /// Turn identifier public var turnId: String /// Tool call identifier @@ -497,7 +564,7 @@ public struct SessionToolCallCompleteAction: Codable, Sendable { } } -public struct SessionToolCallResultConfirmedAction: Codable, Sendable { +public struct ChatToolCallResultConfirmedAction: Codable, Sendable { /// Turn identifier public var turnId: String /// Tool call identifier @@ -536,7 +603,46 @@ public struct SessionToolCallResultConfirmedAction: Codable, Sendable { } } -public struct SessionTurnCompleteAction: Codable, Sendable { +public struct ChatToolCallContentChangedAction: Codable, Sendable { + /// Turn identifier + public var turnId: String + /// Tool call identifier + public var toolCallId: String + /// Additional provider-specific metadata for this tool call. + /// + /// Clients MAY look for well-known keys here to provide enhanced UI. + /// For example, a `ptyTerminal` key with `{ input: string; output: string }` + /// indicates the tool operated on a terminal (both `input` and `output` may + /// contain escape sequences). + public var meta: [String: AnyCodable]? + public var type: ActionType + /// The current partial content for the running tool call + public var content: [ToolResultContent] + + enum CodingKeys: String, CodingKey { + case turnId + case toolCallId + case meta = "_meta" + case type + case content + } + + public init( + turnId: String, + toolCallId: String, + meta: [String: AnyCodable]? = nil, + type: ActionType, + content: [ToolResultContent] + ) { + self.turnId = turnId + self.toolCallId = toolCallId + self.meta = meta + self.type = type + self.content = content + } +} + +public struct ChatTurnCompleteAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -550,7 +656,7 @@ public struct SessionTurnCompleteAction: Codable, Sendable { } } -public struct SessionTurnCancelledAction: Codable, Sendable { +public struct ChatTurnCancelledAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -564,7 +670,7 @@ public struct SessionTurnCancelledAction: Codable, Sendable { } } -public struct SessionErrorAction: Codable, Sendable { +public struct ChatErrorAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -596,7 +702,7 @@ public struct SessionTitleChangedAction: Codable, Sendable { } } -public struct SessionUsageAction: Codable, Sendable { +public struct ChatUsageAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -614,7 +720,7 @@ public struct SessionUsageAction: Codable, Sendable { } } -public struct SessionReasoningAction: Codable, Sendable { +public struct ChatReasoningAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String @@ -763,7 +869,7 @@ public struct SessionActiveClientToolsChangedAction: Codable, Sendable { } } -public struct SessionPendingMessageSetAction: Codable, Sendable { +public struct ChatPendingMessageSetAction: Codable, Sendable { public var type: ActionType /// Whether this is a steering or queued message public var kind: PendingMessageKind @@ -785,7 +891,7 @@ public struct SessionPendingMessageSetAction: Codable, Sendable { } } -public struct SessionPendingMessageRemovedAction: Codable, Sendable { +public struct ChatPendingMessageRemovedAction: Codable, Sendable { public var type: ActionType /// Whether this is a steering or queued message public var kind: PendingMessageKind @@ -803,7 +909,7 @@ public struct SessionPendingMessageRemovedAction: Codable, Sendable { } } -public struct SessionQueuedMessagesReorderedAction: Codable, Sendable { +public struct ChatQueuedMessagesReorderedAction: Codable, Sendable { public var type: ActionType /// Queued message IDs in the desired order public var order: [String] @@ -817,34 +923,34 @@ public struct SessionQueuedMessagesReorderedAction: Codable, Sendable { } } -public struct SessionInputRequestedAction: Codable, Sendable { +public struct ChatInputRequestedAction: Codable, Sendable { public var type: ActionType /// Input request to create or replace - public var request: SessionInputRequest + public var request: ChatInputRequest public init( type: ActionType, - request: SessionInputRequest + request: ChatInputRequest ) { self.type = type self.request = request } } -public struct SessionInputAnswerChangedAction: Codable, Sendable { +public struct ChatInputAnswerChangedAction: Codable, Sendable { public var type: ActionType /// Input request identifier public var requestId: String /// Question identifier within the input request public var questionId: String /// Updated answer, or `undefined` to clear an answer draft - public var answer: SessionInputAnswer? + public var answer: ChatInputAnswer? public init( type: ActionType, requestId: String, questionId: String, - answer: SessionInputAnswer? = nil + answer: ChatInputAnswer? = nil ) { self.type = type self.requestId = requestId @@ -853,20 +959,20 @@ public struct SessionInputAnswerChangedAction: Codable, Sendable { } } -public struct SessionInputCompletedAction: Codable, Sendable { +public struct ChatInputCompletedAction: Codable, Sendable { public var type: ActionType /// Input request identifier public var requestId: String /// Completion outcome - public var response: SessionInputResponseKind + public var response: ChatInputResponseKind /// Optional final answer replacement, keyed by question ID - public var answers: [String: SessionInputAnswer]? + public var answers: [String: ChatInputAnswer]? public init( type: ActionType, requestId: String, - response: SessionInputResponseKind, - answers: [String: SessionInputAnswer]? = nil + response: ChatInputResponseKind, + answers: [String: ChatInputAnswer]? = nil ) { self.type = type self.requestId = requestId @@ -959,7 +1065,7 @@ public struct SessionMcpServerStateChangedAction: Codable, Sendable { } } -public struct SessionTruncatedAction: Codable, Sendable { +public struct ChatTruncatedAction: Codable, Sendable { public var type: ActionType /// Keep turns up to and including this turn. Omit to clear all turns. public var turnId: String? @@ -1010,45 +1116,6 @@ public struct SessionMetaChangedAction: Codable, Sendable { } } -public struct SessionToolCallContentChangedAction: Codable, Sendable { - /// Turn identifier - public var turnId: String - /// Tool call identifier - public var toolCallId: String - /// Additional provider-specific metadata for this tool call. - /// - /// Clients MAY look for well-known keys here to provide enhanced UI. - /// For example, a `ptyTerminal` key with `{ input: string; output: string }` - /// indicates the tool operated on a terminal (both `input` and `output` may - /// contain escape sequences). - public var meta: [String: AnyCodable]? - public var type: ActionType - /// The current partial content for the running tool call - public var content: [ToolResultContent] - - enum CodingKeys: String, CodingKey { - case turnId - case toolCallId - case meta = "_meta" - case type - case content - } - - public init( - turnId: String, - toolCallId: String, - meta: [String: AnyCodable]? = nil, - type: ActionType, - content: [ToolResultContent] - ) { - self.turnId = turnId - self.toolCallId = toolCallId - self.meta = meta - self.type = type - self.content = content - } -} - public struct ChangesetStatusChangedAction: Codable, Sendable { public var type: ActionType /// New computation lifecycle status. @@ -1457,6 +1524,55 @@ public struct ResourceWatchChangedAction: Codable, Sendable { } } +// MARK: - Partial Summary Types + +public struct PartialChatSummary: Codable, Sendable { + /// Chat URI + public var resource: String? + /// Chat title + public var title: String? + /// Current chat status (reuses SessionStatus shape) + public var status: SessionStatus? + /// Human-readable description of what the chat is currently doing + public var activity: String? + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + public var modifiedAt: String? + /// Optional per-chat model override (defaults to the session's model) + public var model: ModelSelection? + /// Optional per-chat agent override (defaults to the session's agent) + public var agent: AgentSelection? + /// How this chat came into existence + public var origin: ChatOrigin? + /// Optional per-chat working directory. + /// + /// If absent, the chat inherits + /// {@link SessionSummary.workingDirectory | the session's working directory}. + /// See {@link ChatState.workingDirectory} for usage notes. + public var workingDirectory: String? + + public init( + resource: String? = nil, + title: String? = nil, + status: SessionStatus? = nil, + activity: String? = nil, + modifiedAt: String? = nil, + model: ModelSelection? = nil, + agent: AgentSelection? = nil, + origin: ChatOrigin? = nil, + workingDirectory: String? = nil + ) { + self.resource = resource + self.title = title + self.status = status + self.activity = activity + self.modifiedAt = modifiedAt + self.model = model + self.agent = agent + self.origin = origin + self.workingDirectory = workingDirectory + } +} + // MARK: - StateAction Union /// Discriminated union of all state actions. @@ -1465,21 +1581,26 @@ public enum StateAction: Codable, Sendable { case rootActiveSessionsChanged(RootActiveSessionsChangedAction) case sessionReady(SessionReadyAction) case sessionCreationFailed(SessionCreationFailedAction) - case sessionTurnStarted(SessionTurnStartedAction) - case sessionDelta(SessionDeltaAction) - case sessionResponsePart(SessionResponsePartAction) - case sessionToolCallStart(SessionToolCallStartAction) - case sessionToolCallDelta(SessionToolCallDeltaAction) - case sessionToolCallReady(SessionToolCallReadyAction) - case sessionToolCallConfirmed(SessionToolCallConfirmedAction) - case sessionToolCallComplete(SessionToolCallCompleteAction) - case sessionToolCallResultConfirmed(SessionToolCallResultConfirmedAction) - case sessionTurnComplete(SessionTurnCompleteAction) - case sessionTurnCancelled(SessionTurnCancelledAction) - case sessionError(SessionErrorAction) + case sessionChatAdded(SessionChatAddedAction) + case sessionChatRemoved(SessionChatRemovedAction) + case sessionChatUpdated(SessionChatUpdatedAction) + case sessionDefaultChatChanged(SessionDefaultChatChangedAction) + case chatTurnStarted(ChatTurnStartedAction) + case chatDelta(ChatDeltaAction) + case chatResponsePart(ChatResponsePartAction) + case chatToolCallStart(ChatToolCallStartAction) + case chatToolCallDelta(ChatToolCallDeltaAction) + case chatToolCallReady(ChatToolCallReadyAction) + case chatToolCallConfirmed(ChatToolCallConfirmedAction) + case chatToolCallComplete(ChatToolCallCompleteAction) + case chatToolCallResultConfirmed(ChatToolCallResultConfirmedAction) + case chatToolCallContentChanged(ChatToolCallContentChangedAction) + case chatTurnComplete(ChatTurnCompleteAction) + case chatTurnCancelled(ChatTurnCancelledAction) + case chatError(ChatErrorAction) case sessionTitleChanged(SessionTitleChangedAction) - case sessionUsage(SessionUsageAction) - case sessionReasoning(SessionReasoningAction) + case chatUsage(ChatUsageAction) + case chatReasoning(ChatReasoningAction) case sessionModelChanged(SessionModelChangedAction) case sessionAgentChanged(SessionAgentChangedAction) case sessionIsReadChanged(SessionIsReadChangedAction) @@ -1489,21 +1610,20 @@ public enum StateAction: Codable, Sendable { case sessionServerToolsChanged(SessionServerToolsChangedAction) case sessionActiveClientChanged(SessionActiveClientChangedAction) case sessionActiveClientToolsChanged(SessionActiveClientToolsChangedAction) - case sessionPendingMessageSet(SessionPendingMessageSetAction) - case sessionPendingMessageRemoved(SessionPendingMessageRemovedAction) - case sessionQueuedMessagesReordered(SessionQueuedMessagesReorderedAction) - case sessionInputRequested(SessionInputRequestedAction) - case sessionInputAnswerChanged(SessionInputAnswerChangedAction) - case sessionInputCompleted(SessionInputCompletedAction) + case chatPendingMessageSet(ChatPendingMessageSetAction) + case chatPendingMessageRemoved(ChatPendingMessageRemovedAction) + case chatQueuedMessagesReordered(ChatQueuedMessagesReorderedAction) + case chatInputRequested(ChatInputRequestedAction) + case chatInputAnswerChanged(ChatInputAnswerChangedAction) + case chatInputCompleted(ChatInputCompletedAction) case sessionCustomizationsChanged(SessionCustomizationsChangedAction) case sessionCustomizationToggled(SessionCustomizationToggledAction) case sessionCustomizationUpdated(SessionCustomizationUpdatedAction) case sessionCustomizationRemoved(SessionCustomizationRemovedAction) - case sessionMcpServerStatusChanged(SessionMcpServerStateChangedAction) - case sessionTruncated(SessionTruncatedAction) + case sessionMcpServerStateChanged(SessionMcpServerStateChangedAction) + case chatTruncated(ChatTruncatedAction) case sessionConfigChanged(SessionConfigChangedAction) case sessionMetaChanged(SessionMetaChangedAction) - case sessionToolCallContentChanged(SessionToolCallContentChangedAction) case changesetStatusChanged(ChangesetStatusChangedAction) case changesetFileSet(ChangesetFileSetAction) case changesetFileRemoved(ChangesetFileRemovedAction) @@ -1546,36 +1666,46 @@ public enum StateAction: Codable, Sendable { self = .sessionReady(try SessionReadyAction(from: decoder)) case "session/creationFailed": self = .sessionCreationFailed(try SessionCreationFailedAction(from: decoder)) - case "session/turnStarted": - self = .sessionTurnStarted(try SessionTurnStartedAction(from: decoder)) - case "session/delta": - self = .sessionDelta(try SessionDeltaAction(from: decoder)) - case "session/responsePart": - self = .sessionResponsePart(try SessionResponsePartAction(from: decoder)) - case "session/toolCallStart": - self = .sessionToolCallStart(try SessionToolCallStartAction(from: decoder)) - case "session/toolCallDelta": - self = .sessionToolCallDelta(try SessionToolCallDeltaAction(from: decoder)) - case "session/toolCallReady": - self = .sessionToolCallReady(try SessionToolCallReadyAction(from: decoder)) - case "session/toolCallConfirmed": - self = .sessionToolCallConfirmed(try SessionToolCallConfirmedAction(from: decoder)) - case "session/toolCallComplete": - self = .sessionToolCallComplete(try SessionToolCallCompleteAction(from: decoder)) - case "session/toolCallResultConfirmed": - self = .sessionToolCallResultConfirmed(try SessionToolCallResultConfirmedAction(from: decoder)) - case "session/turnComplete": - self = .sessionTurnComplete(try SessionTurnCompleteAction(from: decoder)) - case "session/turnCancelled": - self = .sessionTurnCancelled(try SessionTurnCancelledAction(from: decoder)) - case "session/error": - self = .sessionError(try SessionErrorAction(from: decoder)) + case "session/chatAdded": + self = .sessionChatAdded(try SessionChatAddedAction(from: decoder)) + case "session/chatRemoved": + self = .sessionChatRemoved(try SessionChatRemovedAction(from: decoder)) + case "session/chatUpdated": + self = .sessionChatUpdated(try SessionChatUpdatedAction(from: decoder)) + case "session/defaultChatChanged": + self = .sessionDefaultChatChanged(try SessionDefaultChatChangedAction(from: decoder)) + case "chat/turnStarted": + self = .chatTurnStarted(try ChatTurnStartedAction(from: decoder)) + case "chat/delta": + self = .chatDelta(try ChatDeltaAction(from: decoder)) + case "chat/responsePart": + self = .chatResponsePart(try ChatResponsePartAction(from: decoder)) + case "chat/toolCallStart": + self = .chatToolCallStart(try ChatToolCallStartAction(from: decoder)) + case "chat/toolCallDelta": + self = .chatToolCallDelta(try ChatToolCallDeltaAction(from: decoder)) + case "chat/toolCallReady": + self = .chatToolCallReady(try ChatToolCallReadyAction(from: decoder)) + case "chat/toolCallConfirmed": + self = .chatToolCallConfirmed(try ChatToolCallConfirmedAction(from: decoder)) + case "chat/toolCallComplete": + self = .chatToolCallComplete(try ChatToolCallCompleteAction(from: decoder)) + case "chat/toolCallResultConfirmed": + self = .chatToolCallResultConfirmed(try ChatToolCallResultConfirmedAction(from: decoder)) + case "chat/toolCallContentChanged": + self = .chatToolCallContentChanged(try ChatToolCallContentChangedAction(from: decoder)) + case "chat/turnComplete": + self = .chatTurnComplete(try ChatTurnCompleteAction(from: decoder)) + case "chat/turnCancelled": + self = .chatTurnCancelled(try ChatTurnCancelledAction(from: decoder)) + case "chat/error": + self = .chatError(try ChatErrorAction(from: decoder)) case "session/titleChanged": self = .sessionTitleChanged(try SessionTitleChangedAction(from: decoder)) - case "session/usage": - self = .sessionUsage(try SessionUsageAction(from: decoder)) - case "session/reasoning": - self = .sessionReasoning(try SessionReasoningAction(from: decoder)) + case "chat/usage": + self = .chatUsage(try ChatUsageAction(from: decoder)) + case "chat/reasoning": + self = .chatReasoning(try ChatReasoningAction(from: decoder)) case "session/modelChanged": self = .sessionModelChanged(try SessionModelChangedAction(from: decoder)) case "session/agentChanged": @@ -1594,18 +1724,18 @@ public enum StateAction: Codable, Sendable { self = .sessionActiveClientChanged(try SessionActiveClientChangedAction(from: decoder)) case "session/activeClientToolsChanged": self = .sessionActiveClientToolsChanged(try SessionActiveClientToolsChangedAction(from: decoder)) - case "session/pendingMessageSet": - self = .sessionPendingMessageSet(try SessionPendingMessageSetAction(from: decoder)) - case "session/pendingMessageRemoved": - self = .sessionPendingMessageRemoved(try SessionPendingMessageRemovedAction(from: decoder)) - case "session/queuedMessagesReordered": - self = .sessionQueuedMessagesReordered(try SessionQueuedMessagesReorderedAction(from: decoder)) - case "session/inputRequested": - self = .sessionInputRequested(try SessionInputRequestedAction(from: decoder)) - case "session/inputAnswerChanged": - self = .sessionInputAnswerChanged(try SessionInputAnswerChangedAction(from: decoder)) - case "session/inputCompleted": - self = .sessionInputCompleted(try SessionInputCompletedAction(from: decoder)) + case "chat/pendingMessageSet": + self = .chatPendingMessageSet(try ChatPendingMessageSetAction(from: decoder)) + case "chat/pendingMessageRemoved": + self = .chatPendingMessageRemoved(try ChatPendingMessageRemovedAction(from: decoder)) + case "chat/queuedMessagesReordered": + self = .chatQueuedMessagesReordered(try ChatQueuedMessagesReorderedAction(from: decoder)) + case "chat/inputRequested": + self = .chatInputRequested(try ChatInputRequestedAction(from: decoder)) + case "chat/inputAnswerChanged": + self = .chatInputAnswerChanged(try ChatInputAnswerChangedAction(from: decoder)) + case "chat/inputCompleted": + self = .chatInputCompleted(try ChatInputCompletedAction(from: decoder)) case "session/customizationsChanged": self = .sessionCustomizationsChanged(try SessionCustomizationsChangedAction(from: decoder)) case "session/customizationToggled": @@ -1615,15 +1745,13 @@ public enum StateAction: Codable, Sendable { case "session/customizationRemoved": self = .sessionCustomizationRemoved(try SessionCustomizationRemovedAction(from: decoder)) case "session/mcpServerStateChanged": - self = .sessionMcpServerStatusChanged(try SessionMcpServerStateChangedAction(from: decoder)) - case "session/truncated": - self = .sessionTruncated(try SessionTruncatedAction(from: decoder)) + self = .sessionMcpServerStateChanged(try SessionMcpServerStateChangedAction(from: decoder)) + case "chat/truncated": + self = .chatTruncated(try ChatTruncatedAction(from: decoder)) case "session/configChanged": self = .sessionConfigChanged(try SessionConfigChangedAction(from: decoder)) case "session/metaChanged": self = .sessionMetaChanged(try SessionMetaChangedAction(from: decoder)) - case "session/toolCallContentChanged": - self = .sessionToolCallContentChanged(try SessionToolCallContentChangedAction(from: decoder)) case "changeset/statusChanged": self = .changesetStatusChanged(try ChangesetStatusChangedAction(from: decoder)) case "changeset/fileSet": @@ -1685,21 +1813,26 @@ public enum StateAction: Codable, Sendable { case .rootActiveSessionsChanged(let v): try v.encode(to: encoder) case .sessionReady(let v): try v.encode(to: encoder) case .sessionCreationFailed(let v): try v.encode(to: encoder) - case .sessionTurnStarted(let v): try v.encode(to: encoder) - case .sessionDelta(let v): try v.encode(to: encoder) - case .sessionResponsePart(let v): try v.encode(to: encoder) - case .sessionToolCallStart(let v): try v.encode(to: encoder) - case .sessionToolCallDelta(let v): try v.encode(to: encoder) - case .sessionToolCallReady(let v): try v.encode(to: encoder) - case .sessionToolCallConfirmed(let v): try v.encode(to: encoder) - case .sessionToolCallComplete(let v): try v.encode(to: encoder) - case .sessionToolCallResultConfirmed(let v): try v.encode(to: encoder) - case .sessionTurnComplete(let v): try v.encode(to: encoder) - case .sessionTurnCancelled(let v): try v.encode(to: encoder) - case .sessionError(let v): try v.encode(to: encoder) + case .sessionChatAdded(let v): try v.encode(to: encoder) + case .sessionChatRemoved(let v): try v.encode(to: encoder) + case .sessionChatUpdated(let v): try v.encode(to: encoder) + case .sessionDefaultChatChanged(let v): try v.encode(to: encoder) + case .chatTurnStarted(let v): try v.encode(to: encoder) + case .chatDelta(let v): try v.encode(to: encoder) + case .chatResponsePart(let v): try v.encode(to: encoder) + case .chatToolCallStart(let v): try v.encode(to: encoder) + case .chatToolCallDelta(let v): try v.encode(to: encoder) + case .chatToolCallReady(let v): try v.encode(to: encoder) + case .chatToolCallConfirmed(let v): try v.encode(to: encoder) + case .chatToolCallComplete(let v): try v.encode(to: encoder) + case .chatToolCallResultConfirmed(let v): try v.encode(to: encoder) + case .chatToolCallContentChanged(let v): try v.encode(to: encoder) + case .chatTurnComplete(let v): try v.encode(to: encoder) + case .chatTurnCancelled(let v): try v.encode(to: encoder) + case .chatError(let v): try v.encode(to: encoder) case .sessionTitleChanged(let v): try v.encode(to: encoder) - case .sessionUsage(let v): try v.encode(to: encoder) - case .sessionReasoning(let v): try v.encode(to: encoder) + case .chatUsage(let v): try v.encode(to: encoder) + case .chatReasoning(let v): try v.encode(to: encoder) case .sessionModelChanged(let v): try v.encode(to: encoder) case .sessionAgentChanged(let v): try v.encode(to: encoder) case .sessionIsReadChanged(let v): try v.encode(to: encoder) @@ -1709,21 +1842,20 @@ public enum StateAction: Codable, Sendable { case .sessionServerToolsChanged(let v): try v.encode(to: encoder) case .sessionActiveClientChanged(let v): try v.encode(to: encoder) case .sessionActiveClientToolsChanged(let v): try v.encode(to: encoder) - case .sessionPendingMessageSet(let v): try v.encode(to: encoder) - case .sessionPendingMessageRemoved(let v): try v.encode(to: encoder) - case .sessionQueuedMessagesReordered(let v): try v.encode(to: encoder) - case .sessionInputRequested(let v): try v.encode(to: encoder) - case .sessionInputAnswerChanged(let v): try v.encode(to: encoder) - case .sessionInputCompleted(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) + case .chatInputRequested(let v): try v.encode(to: encoder) + case .chatInputAnswerChanged(let v): try v.encode(to: encoder) + case .chatInputCompleted(let v): try v.encode(to: encoder) case .sessionCustomizationsChanged(let v): try v.encode(to: encoder) case .sessionCustomizationToggled(let v): try v.encode(to: encoder) case .sessionCustomizationUpdated(let v): try v.encode(to: encoder) case .sessionCustomizationRemoved(let v): try v.encode(to: encoder) - case .sessionMcpServerStatusChanged(let v): try v.encode(to: encoder) - case .sessionTruncated(let v): try v.encode(to: encoder) + case .sessionMcpServerStateChanged(let v): try v.encode(to: encoder) + case .chatTruncated(let v): try v.encode(to: encoder) case .sessionConfigChanged(let v): try v.encode(to: encoder) case .sessionMetaChanged(let v): try v.encode(to: encoder) - case .sessionToolCallContentChanged(let v): try v.encode(to: encoder) case .changesetStatusChanged(let v): try v.encode(to: encoder) case .changesetFileSet(let v): try v.encode(to: encoder) case .changesetFileRemoved(let v): try v.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 922d1e6f..f03d08f0 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -318,6 +318,63 @@ public struct DisposeSessionParams: Codable, Sendable { } } +public struct ChatForkSource: Codable, Sendable { + /// URI of the existing chat to fork from + public var chat: String + /// Turn ID in the source chat; content up to and including this turn's response is copied + public var turnId: String + + public init( + chat: String, + turnId: String + ) { + self.chat = chat + self.turnId = turnId + } +} + +public struct CreateChatParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Chat URI (client-chosen, e.g. `ahp-chat:/`). + public var chat: String + /// Optional initial message for the new chat. + public var initialMessage: Message? + /// Optional per-chat model override. + public var model: ModelSelection? + /// Optional per-chat agent override. + public var agent: AgentSelection? + /// Optional source chat and turn to fork from. + public var source: ChatForkSource? + + public init( + channel: String, + chat: String, + initialMessage: Message? = nil, + model: ModelSelection? = nil, + agent: AgentSelection? = nil, + source: ChatForkSource? = nil + ) { + self.channel = channel + self.chat = chat + self.initialMessage = initialMessage + self.model = model + self.agent = agent + self.source = source + } +} + +public struct DisposeChatParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + + public init( + channel: String + ) { + self.channel = channel + } +} + public struct ListSessionsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 739e0234..ead87b1d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -162,7 +162,10 @@ public struct PartialSessionSummary: Codable, Sendable { /// Absent (`undefined`) means no custom agent is selected for this session /// — the session uses the provider's default behavior. public var agent: AgentSelection? - /// The working directory URI for this session + /// The default working directory URI for this session. Individual chats + /// MAY override via {@link ChatSummary.workingDirectory | their own + /// `workingDirectory`}; this field acts as the fallback for any chat that + /// does not. public var workingDirectory: String? /// Aggregate summary of file changes associated with this session. Servers /// may populate this to give clients a quick at-a-glance view of the diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index ba335d04..94dfcfb7 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -85,15 +85,21 @@ public struct SessionStatus: OptionSet, Codable, Sendable, Hashable { public static let isArchived = SessionStatus(rawValue: 64) } +public enum ChatOriginKind: String, Codable, Sendable { + case user = "user" + case fork = "fork" + case tool = "tool" +} + /// Answer lifecycle state. -public enum SessionInputAnswerState: String, Codable, Sendable { +public enum ChatInputAnswerState: String, Codable, Sendable { case draft = "draft" case submitted = "submitted" case skipped = "skipped" } /// Answer value kind. -public enum SessionInputAnswerValueKind: String, Codable, Sendable { +public enum ChatInputAnswerValueKind: String, Codable, Sendable { case text = "text" case number = "number" case boolean = "boolean" @@ -102,7 +108,7 @@ public enum SessionInputAnswerValueKind: String, Codable, Sendable { } /// Question/input control kind. -public enum SessionInputQuestionKind: String, Codable, Sendable { +public enum ChatInputQuestionKind: String, Codable, Sendable { case text = "text" case number = "number" case integer = "integer" @@ -112,7 +118,7 @@ public enum SessionInputQuestionKind: String, Codable, Sendable { } /// How a client completed an input request. -public enum SessionInputResponseKind: String, Codable, Sendable { +public enum ChatInputResponseKind: String, Codable, Sendable { case accept = "accept" case decline = "decline" case cancel = "cancel" @@ -722,6 +728,144 @@ public struct PendingMessage: Codable, Sendable { } } +public struct ChatState: Codable, Sendable { + /// Chat URI + public var resource: String + /// Chat title + public var title: String + /// Current chat status (reuses SessionStatus shape) + public var status: SessionStatus + /// Human-readable description of what the chat is currently doing + public var activity: String? + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + public var modifiedAt: String + /// Optional per-chat model override (defaults to the session's model) + public var model: ModelSelection? + /// Optional per-chat agent override (defaults to the session's agent) + public var agent: AgentSelection? + /// How this chat came into existence + public var origin: ChatOrigin? + /// Optional per-chat working directory. + /// + /// If absent, the chat inherits + /// {@link SessionSummary.workingDirectory | the session's working directory}. + /// Hosts MAY override this for individual chats — for example, to give a + /// subordinate chat its own git worktree so multiple chats in a session can + /// make independent edits that the orchestrator later merges back. + public var workingDirectory: String? + /// Completed turns + public var turns: [Turn] + /// Currently in-progress turn + public var activeTurn: ActiveTurn? + /// Message to inject into the current turn at a convenient point + public var steeringMessage: PendingMessage? + /// Messages to send automatically as new turns after the current turn finishes + public var queuedMessages: [PendingMessage]? + /// Requests for user input that are currently blocking or informing chat progress + public var inputRequests: [ChatInputRequest]? + /// Additional provider-specific metadata for this chat. + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case resource + case title + case status + case activity + case modifiedAt + case model + case agent + case origin + case workingDirectory + case turns + case activeTurn + case steeringMessage + case queuedMessages + case inputRequests + case meta = "_meta" + } + + public init( + resource: String, + title: String, + status: SessionStatus, + activity: String? = nil, + modifiedAt: String, + model: ModelSelection? = nil, + agent: AgentSelection? = nil, + origin: ChatOrigin? = nil, + workingDirectory: String? = nil, + turns: [Turn], + activeTurn: ActiveTurn? = nil, + steeringMessage: PendingMessage? = nil, + queuedMessages: [PendingMessage]? = nil, + inputRequests: [ChatInputRequest]? = nil, + meta: [String: AnyCodable]? = nil + ) { + self.resource = resource + self.title = title + self.status = status + self.activity = activity + self.modifiedAt = modifiedAt + self.model = model + self.agent = agent + self.origin = origin + self.workingDirectory = workingDirectory + self.turns = turns + self.activeTurn = activeTurn + self.steeringMessage = steeringMessage + self.queuedMessages = queuedMessages + self.inputRequests = inputRequests + self.meta = meta + } +} + +public struct ChatSummary: Codable, Sendable { + /// Chat URI + public var resource: String + /// Chat title + public var title: String + /// Current chat status (reuses SessionStatus shape) + public var status: SessionStatus + /// Human-readable description of what the chat is currently doing + public var activity: String? + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) + public var modifiedAt: String + /// Optional per-chat model override (defaults to the session's model) + public var model: ModelSelection? + /// Optional per-chat agent override (defaults to the session's agent) + public var agent: AgentSelection? + /// How this chat came into existence + public var origin: ChatOrigin? + /// Optional per-chat working directory. + /// + /// If absent, the chat inherits + /// {@link SessionSummary.workingDirectory | the session's working directory}. + /// See {@link ChatState.workingDirectory} for usage notes. + public var workingDirectory: String? + + public init( + resource: String, + title: String, + status: SessionStatus, + activity: String? = nil, + modifiedAt: String, + model: ModelSelection? = nil, + agent: AgentSelection? = nil, + origin: ChatOrigin? = nil, + workingDirectory: String? = nil + ) { + self.resource = resource + self.title = title + self.status = status + self.activity = activity + self.modifiedAt = modifiedAt + self.model = model + self.agent = agent + self.origin = origin + self.workingDirectory = workingDirectory + } +} + public struct SessionState: Codable, Sendable { /// Lightweight session metadata public var summary: SessionSummary @@ -733,16 +877,13 @@ public struct SessionState: Codable, Sendable { public var serverTools: [ToolDefinition]? /// The client currently providing tools and interactive capabilities to this session public var activeClient: SessionActiveClient? - /// Completed turns - public var turns: [Turn] - /// Currently in-progress turn - public var activeTurn: ActiveTurn? - /// Message to inject into the current turn at a convenient point - public var steeringMessage: PendingMessage? - /// Messages to send automatically as new turns after the current turn finishes - public var queuedMessages: [PendingMessage]? - /// Requests for user input that are currently blocking or informing session progress - public var inputRequests: [SessionInputRequest]? + /// Catalog of chats in this session. + public var chats: [ChatSummary] + /// The chat that receives input when the user addresses the session without + /// selecting a specific chat. This is a UI routing hint, not a hierarchy + /// marker — chats remain equal peers at the protocol level. Hosts MAY change + /// this over the session's lifetime. + public var defaultChat: String? /// Session configuration schema and current values public var config: SessionConfigState? /// Top-level customizations active in this session. @@ -784,11 +925,8 @@ public struct SessionState: Codable, Sendable { case creationError case serverTools case activeClient - case turns - case activeTurn - case steeringMessage - case queuedMessages - case inputRequests + case chats + case defaultChat case config case customizations case changesets @@ -801,11 +939,8 @@ public struct SessionState: Codable, Sendable { creationError: ErrorInfo? = nil, serverTools: [ToolDefinition]? = nil, activeClient: SessionActiveClient? = nil, - turns: [Turn], - activeTurn: ActiveTurn? = nil, - steeringMessage: PendingMessage? = nil, - queuedMessages: [PendingMessage]? = nil, - inputRequests: [SessionInputRequest]? = nil, + chats: [ChatSummary], + defaultChat: String? = nil, config: SessionConfigState? = nil, customizations: [Customization]? = nil, changesets: [Changeset]? = nil, @@ -816,11 +951,8 @@ public struct SessionState: Codable, Sendable { self.creationError = creationError self.serverTools = serverTools self.activeClient = activeClient - self.turns = turns - self.activeTurn = activeTurn - self.steeringMessage = steeringMessage - self.queuedMessages = queuedMessages - self.inputRequests = inputRequests + self.chats = chats + self.defaultChat = defaultChat self.config = config self.customizations = customizations self.changesets = changesets @@ -880,7 +1012,10 @@ public struct SessionSummary: Codable, Sendable { /// Absent (`undefined`) means no custom agent is selected for this session /// — the session uses the provider's default behavior. public var agent: AgentSelection? - /// The working directory URI for this session + /// The default working directory URI for this session. Individual chats + /// MAY override via {@link ChatSummary.workingDirectory | their own + /// `workingDirectory`}; this field acts as the fallback for any chat that + /// does not. public var workingDirectory: String? /// Aggregate summary of file changes associated with this session. Servers /// may populate this to give clients a quick at-a-glance view of the @@ -1066,7 +1201,7 @@ public struct Message: Codable, Sendable { } } -public struct SessionInputOption: Codable, Sendable { +public struct ChatInputOption: Codable, Sendable { /// Stable option identifier; for MCP enum values this is the enum string public var id: String /// Display label @@ -1089,12 +1224,12 @@ public struct SessionInputOption: Codable, Sendable { } } -public struct SessionInputTextAnswerValue: Codable, Sendable { - public var kind: SessionInputAnswerValueKind +public struct ChatInputTextAnswerValue: Codable, Sendable { + public var kind: ChatInputAnswerValueKind public var value: String public init( - kind: SessionInputAnswerValueKind, + kind: ChatInputAnswerValueKind, value: String ) { self.kind = kind @@ -1102,12 +1237,12 @@ public struct SessionInputTextAnswerValue: Codable, Sendable { } } -public struct SessionInputNumberAnswerValue: Codable, Sendable { - public var kind: SessionInputAnswerValueKind +public struct ChatInputNumberAnswerValue: Codable, Sendable { + public var kind: ChatInputAnswerValueKind public var value: Double public init( - kind: SessionInputAnswerValueKind, + kind: ChatInputAnswerValueKind, value: Double ) { self.kind = kind @@ -1115,12 +1250,12 @@ public struct SessionInputNumberAnswerValue: Codable, Sendable { } } -public struct SessionInputBooleanAnswerValue: Codable, Sendable { - public var kind: SessionInputAnswerValueKind +public struct ChatInputBooleanAnswerValue: Codable, Sendable { + public var kind: ChatInputAnswerValueKind public var value: Bool public init( - kind: SessionInputAnswerValueKind, + kind: ChatInputAnswerValueKind, value: Bool ) { self.kind = kind @@ -1128,14 +1263,14 @@ public struct SessionInputBooleanAnswerValue: Codable, Sendable { } } -public struct SessionInputSelectedAnswerValue: Codable, Sendable { - public var kind: SessionInputAnswerValueKind +public struct ChatInputSelectedAnswerValue: Codable, Sendable { + public var kind: ChatInputAnswerValueKind public var value: String /// Free-form text entered instead of selecting an option public var freeformValues: [String]? public init( - kind: SessionInputAnswerValueKind, + kind: ChatInputAnswerValueKind, value: String, freeformValues: [String]? = nil ) { @@ -1145,14 +1280,14 @@ public struct SessionInputSelectedAnswerValue: Codable, Sendable { } } -public struct SessionInputSelectedManyAnswerValue: Codable, Sendable { - public var kind: SessionInputAnswerValueKind +public struct ChatInputSelectedManyAnswerValue: Codable, Sendable { + public var kind: ChatInputAnswerValueKind public var value: [String] /// Free-form text entered in addition to selected options public var freeformValues: [String]? public init( - kind: SessionInputAnswerValueKind, + kind: ChatInputAnswerValueKind, value: [String], freeformValues: [String]? = nil ) { @@ -1162,29 +1297,29 @@ public struct SessionInputSelectedManyAnswerValue: Codable, Sendable { } } -public struct SessionInputAnswered: Codable, Sendable { +public struct ChatInputAnswered: Codable, Sendable { /// Answer state - public var state: SessionInputAnswerState + public var state: ChatInputAnswerState /// Answer value - public var value: SessionInputAnswerValue + public var value: ChatInputAnswerValue public init( - state: SessionInputAnswerState, - value: SessionInputAnswerValue + state: ChatInputAnswerState, + value: ChatInputAnswerValue ) { self.state = state self.value = value } } -public struct SessionInputSkipped: Codable, Sendable { +public struct ChatInputSkipped: Codable, Sendable { /// Answer state - public var state: SessionInputAnswerState + public var state: ChatInputAnswerState /// Free-form reason or value captured while skipping, if any public var freeformValues: [String]? public init( - state: SessionInputAnswerState, + state: ChatInputAnswerState, freeformValues: [String]? = nil ) { self.state = state @@ -1192,7 +1327,7 @@ public struct SessionInputSkipped: Codable, Sendable { } } -public struct SessionInputTextQuestion: Codable, Sendable { +public struct ChatInputTextQuestion: Codable, Sendable { /// Stable question identifier used as the key in `answers` public var id: String /// Short display title @@ -1201,7 +1336,7 @@ public struct SessionInputTextQuestion: Codable, Sendable { public var message: String /// Whether the user must answer this question to accept the request public var required: Bool? - public var kind: SessionInputQuestionKind + public var kind: ChatInputQuestionKind /// Format hint for text questions, such as `email`, `uri`, `date`, or `date-time` public var format: String? /// Minimum string length @@ -1216,7 +1351,7 @@ public struct SessionInputTextQuestion: Codable, Sendable { title: String? = nil, message: String, required: Bool? = nil, - kind: SessionInputQuestionKind, + kind: ChatInputQuestionKind, format: String? = nil, min: Int? = nil, max: Int? = nil, @@ -1234,7 +1369,7 @@ public struct SessionInputTextQuestion: Codable, Sendable { } } -public struct SessionInputNumberQuestion: Codable, Sendable { +public struct ChatInputNumberQuestion: Codable, Sendable { /// Stable question identifier used as the key in `answers` public var id: String /// Short display title @@ -1243,7 +1378,7 @@ public struct SessionInputNumberQuestion: Codable, Sendable { public var message: String /// Whether the user must answer this question to accept the request public var required: Bool? - public var kind: SessionInputQuestionKind + public var kind: ChatInputQuestionKind /// Minimum value public var min: Double? /// Maximum value @@ -1256,7 +1391,7 @@ public struct SessionInputNumberQuestion: Codable, Sendable { title: String? = nil, message: String, required: Bool? = nil, - kind: SessionInputQuestionKind, + kind: ChatInputQuestionKind, min: Double? = nil, max: Double? = nil, defaultValue: Double? = nil @@ -1272,7 +1407,7 @@ public struct SessionInputNumberQuestion: Codable, Sendable { } } -public struct SessionInputBooleanQuestion: Codable, Sendable { +public struct ChatInputBooleanQuestion: Codable, Sendable { /// Stable question identifier used as the key in `answers` public var id: String /// Short display title @@ -1281,7 +1416,7 @@ public struct SessionInputBooleanQuestion: Codable, Sendable { public var message: String /// Whether the user must answer this question to accept the request public var required: Bool? - public var kind: SessionInputQuestionKind + public var kind: ChatInputQuestionKind /// Default boolean value public var defaultValue: Bool? @@ -1290,7 +1425,7 @@ public struct SessionInputBooleanQuestion: Codable, Sendable { title: String? = nil, message: String, required: Bool? = nil, - kind: SessionInputQuestionKind, + kind: ChatInputQuestionKind, defaultValue: Bool? = nil ) { self.id = id @@ -1302,7 +1437,7 @@ public struct SessionInputBooleanQuestion: Codable, Sendable { } } -public struct SessionInputSingleSelectQuestion: Codable, Sendable { +public struct ChatInputSingleSelectQuestion: Codable, Sendable { /// Stable question identifier used as the key in `answers` public var id: String /// Short display title @@ -1311,9 +1446,9 @@ public struct SessionInputSingleSelectQuestion: Codable, Sendable { public var message: String /// Whether the user must answer this question to accept the request public var required: Bool? - public var kind: SessionInputQuestionKind + public var kind: ChatInputQuestionKind /// Options the user may select from - public var options: [SessionInputOption] + public var options: [ChatInputOption] /// Whether the user may enter text instead of selecting an option public var allowFreeformInput: Bool? @@ -1322,8 +1457,8 @@ public struct SessionInputSingleSelectQuestion: Codable, Sendable { title: String? = nil, message: String, required: Bool? = nil, - kind: SessionInputQuestionKind, - options: [SessionInputOption], + kind: ChatInputQuestionKind, + options: [ChatInputOption], allowFreeformInput: Bool? = nil ) { self.id = id @@ -1336,7 +1471,7 @@ public struct SessionInputSingleSelectQuestion: Codable, Sendable { } } -public struct SessionInputMultiSelectQuestion: Codable, Sendable { +public struct ChatInputMultiSelectQuestion: Codable, Sendable { /// Stable question identifier used as the key in `answers` public var id: String /// Short display title @@ -1345,9 +1480,9 @@ public struct SessionInputMultiSelectQuestion: Codable, Sendable { public var message: String /// Whether the user must answer this question to accept the request public var required: Bool? - public var kind: SessionInputQuestionKind + public var kind: ChatInputQuestionKind /// Options the user may select from - public var options: [SessionInputOption] + public var options: [ChatInputOption] /// Whether the user may enter text in addition to selecting options public var allowFreeformInput: Bool? /// Minimum selected item count @@ -1360,8 +1495,8 @@ public struct SessionInputMultiSelectQuestion: Codable, Sendable { title: String? = nil, message: String, required: Bool? = nil, - kind: SessionInputQuestionKind, - options: [SessionInputOption], + kind: ChatInputQuestionKind, + options: [ChatInputOption], allowFreeformInput: Bool? = nil, min: Int? = nil, max: Int? = nil @@ -1378,7 +1513,7 @@ public struct SessionInputMultiSelectQuestion: Codable, Sendable { } } -public struct SessionInputRequest: Codable, Sendable { +public struct ChatInputRequest: Codable, Sendable { /// Stable request identifier public var id: String /// Display message for the request as a whole @@ -1386,16 +1521,16 @@ public struct SessionInputRequest: Codable, Sendable { /// URL the user should review or open, for URL-style elicitations public var url: String? /// Ordered questions to ask the user - public var questions: [SessionInputQuestion]? + public var questions: [ChatInputQuestion]? /// Current draft or submitted answers, keyed by question ID - public var answers: [String: SessionInputAnswer]? + public var answers: [String: ChatInputAnswer]? public init( id: String, message: String? = nil, url: String? = nil, - questions: [SessionInputQuestion]? = nil, - answers: [String: SessionInputAnswer]? = nil + questions: [ChatInputQuestion]? = nil, + answers: [String: ChatInputAnswer]? = nil ) { self.id = id self.message = message @@ -1714,7 +1849,7 @@ public struct MessageAnnotationsAttachment: Codable, Sendable { public struct MarkdownResponsePart: Codable, Sendable { /// Discriminant public var kind: ResponsePartKind - /// Part identifier, used by `session/delta` to target this part for content appends + /// Part identifier, used by `chat/delta` to target this part for content appends public var id: String /// Markdown content public var content: String @@ -1790,7 +1925,7 @@ public struct ToolCallResponsePart: Codable, Sendable { public struct ReasoningResponsePart: Codable, Sendable { /// Discriminant public var kind: ResponsePartKind - /// Part identifier, used by `session/reasoning` to target this part for content appends + /// Part identifier, used by `chat/reasoning` to target this part for content appends public var id: String /// Accumulated reasoning text public var content: String @@ -3234,7 +3369,7 @@ public struct ToolCallClientContributor: Codable, Sendable { /// Absent for server-side tools. /// /// When set, the identified client is responsible for executing the tool and - /// dispatching `session/toolCallComplete` with the result. + /// dispatching `chat/toolCallComplete` with the result. public var clientId: String public init( @@ -3499,7 +3634,7 @@ public struct ErrorInfo: Codable, Sendable { } public struct Snapshot: Codable, Sendable { - /// The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`) + /// The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`) public var resource: String /// The current state of the resource public var state: SnapshotState @@ -3878,6 +4013,66 @@ public struct ResourceChange: Codable, Sendable { // MARK: - Discriminated Unions +public struct ChatOriginUser: Codable, Sendable { + public var kind: ChatOriginKind + + public init(kind: ChatOriginKind = .user) { + self.kind = kind + } +} + +public struct ChatOriginFork: Codable, Sendable { + public var kind: ChatOriginKind + public var chat: String + public var turnId: String + + public init(kind: ChatOriginKind = .fork, chat: String, turnId: String) { + self.kind = kind + self.chat = chat + self.turnId = turnId + } +} + +public struct ChatOriginTool: Codable, Sendable { + public var kind: ChatOriginKind + public var chat: String + public var toolCallId: String + + public init(kind: ChatOriginKind = .tool, chat: String, toolCallId: String) { + self.kind = kind + self.chat = chat + self.toolCallId = toolCallId + } +} + +public enum ChatOrigin: Codable, Sendable { + case user(ChatOriginUser) + case fork(ChatOriginFork) + case tool(ChatOriginTool) + + private enum DiscriminatorCodingKeys: String, CodingKey { case kind } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminatorCodingKeys.self) + let discriminant = try container.decode(String.self, forKey: .kind) + switch discriminant { + case "user": self = .user(try ChatOriginUser(from: decoder)) + case "fork": self = .fork(try ChatOriginFork(from: decoder)) + case "tool": self = .tool(try ChatOriginTool(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown ChatOrigin kind: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .user(let value): try value.encode(to: encoder) + case .fork(let value): try value.encode(to: encoder) + case .tool(let value): try value.encode(to: encoder) + } + } +} + public enum ResponsePart: Codable, Sendable { case markdown(MarkdownResponsePart) case contentRef(ResourceReponsePart) @@ -4022,13 +4217,13 @@ public enum TerminalContentPart: Codable, Sendable { } } -public enum SessionInputQuestion: Codable, Sendable { - case text(SessionInputTextQuestion) - case number(SessionInputNumberQuestion) - case integer(SessionInputNumberQuestion) - case boolean(SessionInputBooleanQuestion) - case singleSelect(SessionInputSingleSelectQuestion) - case multiSelect(SessionInputMultiSelectQuestion) +public enum ChatInputQuestion: Codable, Sendable { + case text(ChatInputTextQuestion) + case number(ChatInputNumberQuestion) + case integer(ChatInputNumberQuestion) + case boolean(ChatInputBooleanQuestion) + case singleSelect(ChatInputSingleSelectQuestion) + case multiSelect(ChatInputMultiSelectQuestion) private enum DiscriminantKey: String, CodingKey { case discriminant = "kind" @@ -4039,19 +4234,19 @@ public enum SessionInputQuestion: Codable, Sendable { let discriminant = try container.decode(String.self, forKey: .discriminant) switch discriminant { case "text": - self = .text(try SessionInputTextQuestion(from: decoder)) + self = .text(try ChatInputTextQuestion(from: decoder)) case "number": - self = .number(try SessionInputNumberQuestion(from: decoder)) + self = .number(try ChatInputNumberQuestion(from: decoder)) case "integer": - self = .integer(try SessionInputNumberQuestion(from: decoder)) + self = .integer(try ChatInputNumberQuestion(from: decoder)) case "boolean": - self = .boolean(try SessionInputBooleanQuestion(from: decoder)) + self = .boolean(try ChatInputBooleanQuestion(from: decoder)) case "single-select": - self = .singleSelect(try SessionInputSingleSelectQuestion(from: decoder)) + self = .singleSelect(try ChatInputSingleSelectQuestion(from: decoder)) case "multi-select": - self = .multiSelect(try SessionInputMultiSelectQuestion(from: decoder)) + self = .multiSelect(try ChatInputMultiSelectQuestion(from: decoder)) default: - throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown SessionInputQuestion discriminant: \(discriminant)") + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown ChatInputQuestion discriminant: \(discriminant)") } } @@ -4067,12 +4262,12 @@ public enum SessionInputQuestion: Codable, Sendable { } } -public enum SessionInputAnswerValue: Codable, Sendable { - case text(SessionInputTextAnswerValue) - case number(SessionInputNumberAnswerValue) - case boolean(SessionInputBooleanAnswerValue) - case selected(SessionInputSelectedAnswerValue) - case selectedMany(SessionInputSelectedManyAnswerValue) +public enum ChatInputAnswerValue: Codable, Sendable { + case text(ChatInputTextAnswerValue) + case number(ChatInputNumberAnswerValue) + case boolean(ChatInputBooleanAnswerValue) + case selected(ChatInputSelectedAnswerValue) + case selectedMany(ChatInputSelectedManyAnswerValue) private enum DiscriminantKey: String, CodingKey { case discriminant = "kind" @@ -4083,17 +4278,17 @@ public enum SessionInputAnswerValue: Codable, Sendable { let discriminant = try container.decode(String.self, forKey: .discriminant) switch discriminant { case "text": - self = .text(try SessionInputTextAnswerValue(from: decoder)) + self = .text(try ChatInputTextAnswerValue(from: decoder)) case "number": - self = .number(try SessionInputNumberAnswerValue(from: decoder)) + self = .number(try ChatInputNumberAnswerValue(from: decoder)) case "boolean": - self = .boolean(try SessionInputBooleanAnswerValue(from: decoder)) + self = .boolean(try ChatInputBooleanAnswerValue(from: decoder)) case "selected": - self = .selected(try SessionInputSelectedAnswerValue(from: decoder)) + self = .selected(try ChatInputSelectedAnswerValue(from: decoder)) case "selected-many": - self = .selectedMany(try SessionInputSelectedManyAnswerValue(from: decoder)) + self = .selectedMany(try ChatInputSelectedManyAnswerValue(from: decoder)) default: - throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown SessionInputAnswerValue discriminant: \(discriminant)") + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown ChatInputAnswerValue discriminant: \(discriminant)") } } @@ -4108,10 +4303,10 @@ public enum SessionInputAnswerValue: Codable, Sendable { } } -public enum SessionInputAnswer: Codable, Sendable { - case draft(SessionInputAnswered) - case submitted(SessionInputAnswered) - case skipped(SessionInputSkipped) +public enum ChatInputAnswer: Codable, Sendable { + case draft(ChatInputAnswered) + case submitted(ChatInputAnswered) + case skipped(ChatInputSkipped) private enum DiscriminantKey: String, CodingKey { case discriminant = "state" @@ -4122,13 +4317,13 @@ public enum SessionInputAnswer: Codable, Sendable { let discriminant = try container.decode(String.self, forKey: .discriminant) switch discriminant { case "draft": - self = .draft(try SessionInputAnswered(from: decoder)) + self = .draft(try ChatInputAnswered(from: decoder)) case "submitted": - self = .submitted(try SessionInputAnswered(from: decoder)) + self = .submitted(try ChatInputAnswered(from: decoder)) case "skipped": - self = .skipped(try SessionInputSkipped(from: decoder)) + self = .skipped(try ChatInputSkipped(from: decoder)) default: - throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown SessionInputAnswer discriminant: \(discriminant)") + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown ChatInputAnswer discriminant: \(discriminant)") } } @@ -4417,10 +4612,11 @@ public enum ToolResultContent: Codable, Sendable { } } -/// The state payload of a snapshot — root, session, terminal, changeset, resource-watch, or annotations state. +/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, or annotations state. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) + case chat(ChatState) case terminal(TerminalState) case changeset(ChangesetState) case resourceWatch(ResourceWatchState) @@ -4447,6 +4643,7 @@ public enum SnapshotState: Codable, Sendable { switch self { case .root(let state): try state.encode(to: encoder) case .session(let state): try state.encode(to: encoder) + case .chat(let state): try state.encode(to: encoder) case .terminal(let state): try state.encode(to: encoder) case .changeset(let state): try state.encode(to: encoder) case .resourceWatch(let state): try state.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift index 2afac4ce..4b48352f 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift @@ -109,696 +109,32 @@ public struct AHPRootReducer: Reducer { } } -// MARK: - Session Reducer (Protocol-based) -/// Protocol-based session reducer for AHP session state. -/// -/// This is a native Swift implementation of the session reducer using -/// idiomatic patterns: -/// - `inout` mutation instead of copy-on-write spread -/// - Guard-based early returns -/// - Pattern matching on enums -/// - Helper methods as private functions -public struct AHPSessionReducer: Reducer { - public typealias State = SessionState +// MARK: - Chat Reducer (Protocol-based) + +/// Protocol-based chat reducer for AHP chat state. +public struct AHPChatReducer: Reducer { + public typealias State = ChatState public typealias Action = StateAction public init() {} - public func reduce(into state: inout SessionState, action: StateAction) { - switch action { - - // ── Lifecycle ────────────────────────────────────────────────────────── - - case .sessionReady: - state.lifecycle = .ready - state.summary.status = .idle - - case .sessionCreationFailed(let a): - state.lifecycle = .creationFailed - state.creationError = a.error - - // ── Turn Lifecycle ──────────────────────────────────────────────────── - - case .sessionTurnStarted(let a): - state.summary.modifiedAt = currentTimestamp() - state.activeTurn = ActiveTurn( - id: a.turnId, - message: a.message, - responseParts: [], - usage: nil - ) - // If auto-started from a pending message, remove it - if let queuedId = a.queuedMessageId { - if state.steeringMessage?.id == queuedId { - state.steeringMessage = nil - } - if var queued = state.queuedMessages { - queued.removeAll { $0.id == queuedId } - state.queuedMessages = queued.isEmpty ? nil : queued - } - } - state.summary.status = Self.withStatusFlag(Self.sessionSummaryStatus(state), .isRead, false) - - case .sessionDelta(let a): - Self.updateResponsePartInPlace(state: &state, turnId: a.turnId, partId: a.partId) { part in - guard case .markdown(var md) = part else { return } - md.content += a.content - part = .markdown(md) - } - - case .sessionResponsePart(let a): - guard state.activeTurn?.id == a.turnId else { return } - state.activeTurn?.responseParts.append(a.part) - - case .sessionTurnComplete(let a): - Self.endTurn(state: &state, turnId: a.turnId, turnState: .complete) - - case .sessionTurnCancelled(let a): - Self.endTurn(state: &state, turnId: a.turnId, turnState: .cancelled) - - case .sessionError(let a): - Self.endTurn(state: &state, turnId: a.turnId, turnState: .error, terminalStatus: .error, error: a.error) - - // ── Tool Call State Machine ─────────────────────────────────────────── - - case .sessionToolCallStart(let a): - guard state.activeTurn?.id == a.turnId else { return } - let toolCallPart = ToolCallResponsePart( - kind: .toolCall, - toolCall: .streaming(ToolCallStreamingState( - toolCallId: a.toolCallId, - toolName: a.toolName, - displayName: a.displayName, - contributor: a.contributor, - meta: a.meta, - status: .streaming - )) - ) - state.activeTurn?.responseParts.append(.toolCall(toolCallPart)) - - case .sessionToolCallDelta(let a): - Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - guard case .streaming(var s) = tc else { return } - s.meta = a.meta ?? s.meta - s.partialInput = (s.partialInput ?? "") + a.content - if let msg = a.invocationMessage { - s.invocationMessage = msg - } - tc = .streaming(s) - } - - case .sessionToolCallReady(let a): - Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - // Only process if currently streaming or running - switch tc { - case .streaming, .running: break - default: return - } - let base = tc.baseFields - let meta = a.meta ?? base.meta - if let confirmed = a.confirmed { - tc = .running(ToolCallRunningState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: a.invocationMessage, - toolInput: a.toolInput, - status: .running, - confirmed: confirmed - )) - } else { - tc = .pendingConfirmation(ToolCallPendingConfirmationState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: a.invocationMessage, - toolInput: a.toolInput, - status: .pendingConfirmation, - confirmationTitle: a.confirmationTitle, - edits: a.edits, - editable: a.editable, - options: a.options - )) - } - } - Self.refreshSummaryStatus(&state) - - case .sessionToolCallConfirmed(let a): - Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - guard case .pendingConfirmation(let pending) = tc else { return } - let base = tc.baseFields - let meta = a.meta ?? base.meta - let selectedOption = Self.resolveSelectedOption(pending.options, id: a.selectedOptionId) - if a.approved { - tc = .running(ToolCallRunningState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: pending.invocationMessage, - toolInput: a.editedToolInput ?? pending.toolInput, - status: .running, - confirmed: a.confirmed ?? .notNeeded, - selectedOption: selectedOption - )) - } else { - tc = .cancelled(ToolCallCancelledState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: pending.invocationMessage, - toolInput: pending.toolInput, - status: .cancelled, - reason: a.reason ?? .denied, - reasonMessage: a.reasonMessage, - userSuggestion: a.userSuggestion, - selectedOption: selectedOption - )) - } - } - Self.refreshSummaryStatus(&state) - - case .sessionToolCallComplete(let a): - Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - let base = tc.baseFields - let meta = a.meta ?? base.meta - let confirmed: ToolCallConfirmationReason - let invocationMessage: StringOrMarkdown - let toolInput: String? - let selectedOption: ConfirmationOption? - switch tc { - case .running(let r): - confirmed = r.confirmed - invocationMessage = r.invocationMessage - toolInput = r.toolInput - selectedOption = r.selectedOption - case .pendingConfirmation(let p): - confirmed = .notNeeded - invocationMessage = p.invocationMessage - toolInput = p.toolInput - selectedOption = nil - default: - return - } - - if a.requiresResultConfirmation == true { - tc = .pendingResultConfirmation(ToolCallPendingResultConfirmationState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: invocationMessage, - toolInput: toolInput, - success: a.result.success, - pastTenseMessage: a.result.pastTenseMessage, - content: a.result.content, - structuredContent: a.result.structuredContent, - error: a.result.error, - status: .pendingResultConfirmation, - confirmed: confirmed, - selectedOption: selectedOption - )) - } else { - tc = .completed(ToolCallCompletedState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: invocationMessage, - toolInput: toolInput, - success: a.result.success, - pastTenseMessage: a.result.pastTenseMessage, - content: a.result.content, - structuredContent: a.result.structuredContent, - error: a.result.error, - status: .completed, - confirmed: confirmed, - selectedOption: selectedOption - )) - } - } - Self.refreshSummaryStatus(&state) - - case .sessionToolCallResultConfirmed(let a): - Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - guard case .pendingResultConfirmation(let prc) = tc else { return } - let base = tc.baseFields - let meta = a.meta ?? base.meta - if a.approved { - tc = .completed(ToolCallCompletedState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: prc.invocationMessage, - toolInput: prc.toolInput, - success: prc.success, - pastTenseMessage: prc.pastTenseMessage, - content: prc.content, - structuredContent: prc.structuredContent, - error: prc.error, - status: .completed, - confirmed: prc.confirmed, - selectedOption: prc.selectedOption - )) - } else { - tc = .cancelled(ToolCallCancelledState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: meta, - invocationMessage: prc.invocationMessage, - toolInput: prc.toolInput, - status: .cancelled, - reason: .resultDenied, - selectedOption: prc.selectedOption - )) - } - } - Self.refreshSummaryStatus(&state) - - // ── Metadata ────────────────────────────────────────────────────────── - - case .sessionTitleChanged(let a): - state.summary.title = a.title - state.summary.modifiedAt = currentTimestamp() - - case .sessionUsage(let a): - guard state.activeTurn?.id == a.turnId else { return } - state.activeTurn?.usage = a.usage - - case .sessionReasoning(let a): - Self.updateResponsePartInPlace(state: &state, turnId: a.turnId, partId: a.partId) { part in - guard case .reasoning(var r) = part else { return } - r.content += a.content - part = .reasoning(r) - } - - case .sessionModelChanged(let a): - state.summary.model = a.model - state.summary.modifiedAt = currentTimestamp() - - case .sessionAgentChanged(let a): - state.summary.agent = a.agent - state.summary.modifiedAt = currentTimestamp() - - case .sessionActivityChanged(let a): - state.summary.activity = a.activity - - case .sessionConfigChanged(let a): - guard var config = state.config else { return } - config.values = a.replace == true ? a.config : config.values.merging(a.config) { _, new in new } - state.config = config - state.summary.modifiedAt = currentTimestamp() - - case .sessionMetaChanged(let a): - state.meta = a.meta - - case .sessionServerToolsChanged(let a): - state.serverTools = a.tools - - case .sessionActiveClientChanged(let a): - state.activeClient = a.activeClient - - case .sessionActiveClientToolsChanged(let a): - guard state.activeClient != nil else { return } - state.activeClient?.tools = a.tools - - // ── Customizations ────────────────────────────────────────────────── - - case .sessionCustomizationsChanged(let a): - state.customizations = a.customizations - - case .sessionCustomizationToggled(let a): - guard var list = state.customizations else { return } - if toggleCustomization(in: &list, id: a.id, enabled: a.enabled) { - state.customizations = list - } - - case .sessionCustomizationUpdated(let a): - var list = state.customizations ?? [] - if let idx = list.firstIndex(where: { customizationId($0) == customizationId(a.customization) }) { - list[idx] = a.customization - } else { - list.append(a.customization) - } - state.customizations = list - - case .sessionCustomizationRemoved(let a): - guard var list = state.customizations else { return } - if let idx = list.firstIndex(where: { customizationId($0) == a.id }) { - list.remove(at: idx) - state.customizations = list - return - } - for containerIdx in list.indices { - var container = list[containerIdx] - guard var children = customizationChildren(container) else { continue } - if let idx = children.firstIndex(where: { childId($0) == a.id }) { - children.remove(at: idx) - setCustomizationChildren(&container, children) - list[containerIdx] = container - state.customizations = list - return - } - } - - case .sessionMcpServerStatusChanged(let a): - guard var list = state.customizations else { return } - if let topIdx = list.firstIndex(where: { customizationId($0) == a.id }) { - guard case .mcpServer(var entry) = list[topIdx] else { return } - entry.state = a.state - entry.channel = a.channel - list[topIdx] = .mcpServer(entry) - state.customizations = list - return - } - for containerIdx in list.indices { - var container = list[containerIdx] - guard var children = customizationChildren(container) else { continue } - guard let childIdx = children.firstIndex(where: { childId($0) == a.id }) else { continue } - guard case .mcpServer(var child) = children[childIdx] else { continue } - child.state = a.state - child.channel = a.channel - children[childIdx] = .mcpServer(child) - setCustomizationChildren(&container, children) - list[containerIdx] = container - state.customizations = list - return - } - - // ── Truncation ──────────────────────────────────────────────────────── - - case .sessionTruncated(let a): - let turns: [Turn] - if let turnId = a.turnId { - guard let idx = state.turns.firstIndex(where: { $0.id == turnId }) else { - return - } - turns = Array(state.turns.prefix(idx + 1)) - } else { - turns = [] - } - state.turns = turns - state.activeTurn = nil - state.inputRequests = nil - state.summary.status = Self.sessionSummaryStatus(state) - state.summary.modifiedAt = currentTimestamp() - - // ── Read / Archived ───────────────────────────────────────────────── - - case .sessionIsReadChanged(let a): - state.summary.status = Self.withStatusFlag(state.summary.status, .isRead, a.isRead) - - case .sessionIsArchivedChanged(let a): - state.summary.status = Self.withStatusFlag(state.summary.status, .isArchived, a.isArchived) - - - // ── Tool Call Content ──────────────────────────────────────────────── - - case .sessionToolCallContentChanged(let a): - Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - guard case .running(var r) = tc else { return } - r.meta = a.meta ?? r.meta - r.content = a.content - tc = .running(r) - } - - // ── Pending Messages ────────────────────────────────────────────────── - - case .sessionPendingMessageSet(let a): - let entry = PendingMessage(id: a.id, message: a.message) - if a.kind == .steering { - state.steeringMessage = entry - return - } - if let idx = state.queuedMessages?.firstIndex(where: { $0.id == a.id }) { - state.queuedMessages?[idx] = entry - } else { - var existing = state.queuedMessages ?? [] - existing.append(entry) - state.queuedMessages = existing - } - - case .sessionPendingMessageRemoved(let a): - if a.kind == .steering { - guard state.steeringMessage?.id == a.id else { return } - state.steeringMessage = nil - return - } - guard var existing = state.queuedMessages else { return } - let before = existing.count - existing.removeAll { $0.id == a.id } - guard existing.count != before else { return } - state.queuedMessages = existing.isEmpty ? nil : existing - - case .sessionQueuedMessagesReordered(let a): - guard let existing = state.queuedMessages else { return } - let byId = Dictionary(uniqueKeysWithValues: existing.map { ($0.id, $0) }) - var ordered = Set() - var reordered: [PendingMessage] = a.order.compactMap { id in - guard let msg = byId[id], !ordered.contains(id) else { return nil } - ordered.insert(id) - return msg - } - for m in existing where !ordered.contains(m.id) { - reordered.append(m) - } - state.queuedMessages = reordered - - // ── Session Input Requests ───────────────────────────────────────────── - - case .sessionInputRequested(let a): - Self.upsertInputRequest(state: &state, request: a.request) - - case .sessionInputAnswerChanged(let a): - guard var existing = state.inputRequests, - let idx = existing.firstIndex(where: { $0.id == a.requestId }) else { - return - } - var request = existing[idx] - var answers = request.answers ?? [:] - if let answer = a.answer { - answers[a.questionId] = answer - } else { - answers.removeValue(forKey: a.questionId) - } - request.answers = answers.isEmpty ? nil : answers - existing[idx] = request - state.inputRequests = existing - state.summary.modifiedAt = currentTimestamp() - - case .sessionInputCompleted(let a): - guard var existing = state.inputRequests, - existing.contains(where: { $0.id == a.requestId }) else { - return - } - existing.removeAll { $0.id == a.requestId } - state.inputRequests = existing.isEmpty ? nil : existing - state.summary.status = Self.sessionSummaryStatus(state) - state.summary.modifiedAt = currentTimestamp() - - default: - break - } - } - - // MARK: - Private Helpers - - /// Bitmask covering the mutually-exclusive activity bits (bits 0–4). - private static let statusActivityMask = SessionStatus(rawValue: (1 << 5) - 1) - - /// Sets or clears a metadata flag on a status value. - private static func withStatusFlag(_ status: SessionStatus, _ flag: SessionStatus, _ set: Bool) -> SessionStatus { - set ? status.union(flag) : status.subtracting(flag) - } - - // ToolCallBaseFields and toolCallBase() are now shared via - // ToolCallState.baseFields in ToolCallStateExtensions.swift. - - /// Ends the active turn, producing a completed Turn record. - /// Non-terminal tool calls are forced to cancelled. - private static func endTurn( - state: inout SessionState, - turnId: String, - turnState: TurnState, - terminalStatus: SessionStatus? = nil, - error: ErrorInfo? = nil - ) { - guard let activeTurn = state.activeTurn, activeTurn.id == turnId else { return } - - let responseParts: [ResponsePart] = activeTurn.responseParts.map { part in - guard case .toolCall(let tcPart) = part else { return part } - let tc = tcPart.toolCall - switch tc { - case .completed, .cancelled: - return part - default: - let base = tc.baseFields - let invocationMessage: StringOrMarkdown - let toolInput: String? - switch tc { - case .streaming(let s): - invocationMessage = s.invocationMessage ?? .string("") - toolInput = nil - case .pendingConfirmation(let p): - invocationMessage = p.invocationMessage - toolInput = p.toolInput - case .running(let r): - invocationMessage = r.invocationMessage - toolInput = r.toolInput - case .pendingResultConfirmation(let r): - invocationMessage = r.invocationMessage - toolInput = r.toolInput - default: - invocationMessage = .string("") - toolInput = nil - } - return .toolCall(ToolCallResponsePart( - kind: .toolCall, - toolCall: .cancelled(ToolCallCancelledState( - toolCallId: base.toolCallId, - toolName: base.toolName, - displayName: base.displayName, - contributor: base.contributor, - meta: base.meta, - invocationMessage: invocationMessage, - toolInput: toolInput, - status: .cancelled, - reason: .skipped - )) - )) - } - } - - let turn = Turn( - id: activeTurn.id, - message: activeTurn.message, - responseParts: responseParts, - usage: activeTurn.usage, - state: turnState, - error: error - ) - - state.turns.append(turn) - state.activeTurn = nil - state.inputRequests = nil - state.summary.status = Self.sessionSummaryStatus(state, terminalStatus: terminalStatus) - state.summary.modifiedAt = currentTimestamp() - } - - private static func sessionSummaryStatus(_ state: SessionState, terminalStatus: SessionStatus? = nil) -> SessionStatus { - let activity: SessionStatus - if let terminalStatus { - activity = terminalStatus - } else if state.inputRequests?.isEmpty == false || Self.hasPendingToolCallConfirmation(state) { - activity = .inputNeeded - } else if state.activeTurn != nil { - activity = .inProgress - } else { - activity = .idle - } - return state.summary.status.subtracting(Self.statusActivityMask).union(activity) - } - - /// Returns `true` if the active turn has any tool call awaiting user confirmation. - private static func hasPendingToolCallConfirmation(_ state: SessionState) -> Bool { - guard let activeTurn = state.activeTurn else { return false } - for part in activeTurn.responseParts { - guard case .toolCall(let tcPart) = part else { continue } - switch tcPart.toolCall { - case .pendingConfirmation, .pendingResultConfirmation: - return true - default: - continue - } - } - return false - } - - private static func resolveSelectedOption(_ options: [ConfirmationOption]?, id: String?) -> ConfirmationOption? { - guard let id, let options else { - return nil - } - return options.first { $0.id == id } - } - - /// Recomputes `summary.status` from the current state in place. - private static func refreshSummaryStatus(_ state: inout SessionState) { - state.summary.status = Self.sessionSummaryStatus(state) - } - - private static func upsertInputRequest(state: inout SessionState, request: SessionInputRequest) { - var existing = state.inputRequests ?? [] - if let idx = existing.firstIndex(where: { $0.id == request.id }) { - var replacement = request - replacement.answers = request.answers ?? existing[idx].answers - existing[idx] = replacement - } else { - existing.append(request) - } - state.inputRequests = existing - state.summary.status = Self.withStatusFlag(Self.sessionSummaryStatus(state), .isRead, false) - state.summary.modifiedAt = currentTimestamp() + public func reduce(into state: inout ChatState, action: StateAction) { + state = chatReducer(state: state, action: action) } +} - /// Updates a tool call inside the active turn's response parts in place. - private static func updateToolCallInPlace( - state: inout SessionState, - turnId: String, - toolCallId: String, - updater: (inout ToolCallState) -> Void - ) { - guard state.activeTurn?.id == turnId else { return } - guard let parts = state.activeTurn?.responseParts else { return } - - var found = false - let newParts: [ResponsePart] = parts.map { part in - guard case .toolCall(var tcPart) = part else { return part } - guard tcPart.toolCall.toolCallId == toolCallId else { return part } - found = true - updater(&tcPart.toolCall) - return .toolCall(tcPart) - } +// MARK: - Session Reducer (Protocol-based) - guard found else { return } - state.activeTurn?.responseParts = newParts - } +/// Protocol-based session reducer for AHP session state. +public struct AHPSessionReducer: Reducer { + public typealias State = SessionState + public typealias Action = StateAction - /// Updates a response part identified by partId in the active turn in place. - private static func updateResponsePartInPlace( - state: inout SessionState, - turnId: String, - partId: String, - updater: (inout ResponsePart) -> Void - ) { - guard state.activeTurn?.id == turnId else { return } - guard let parts = state.activeTurn?.responseParts else { return } - - var found = false - let newParts: [ResponsePart] = parts.map { part in - guard !found else { return part } - guard part.partId == partId else { return part } - found = true - var mutable = part - updater(&mutable) - return mutable - } + public init() {} - guard found else { return } - state.activeTurn?.responseParts = newParts + public func reduce(into state: inout SessionState, action: StateAction) { + state = sessionReducer(state: state, action: action) } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 0b91b0ca..ff616048 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -10,6 +10,13 @@ public var currentTimestampProvider: () -> Int = { Int(Date().timeIntervalSince1970 * 1000) } +private let iso8601TimestampFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter +}() + // MARK: - Status Bitset Helpers /// Bitmask covering the mutually-exclusive activity bits (bits 0–4). @@ -62,39 +69,24 @@ public func rootReducer(state: RootState, action: StateAction) -> RootState { } } -// MARK: - Session Reducer - -/// Pure reducer for session state. -public func sessionReducer(state: SessionState, action: StateAction) -> SessionState { - switch action { - - // ── Lifecycle ────────────────────────────────────────────────────────── - case .sessionReady: - // Lifecycle-only transition. Must not touch `summary.status`: see - // the equivalent TypeScript reducer for the rationale. - var next = state - next.lifecycle = .ready - return next +// MARK: - Chat Reducer - case .sessionCreationFailed(let a): - var next = state - next.lifecycle = .creationFailed - next.creationError = a.error - return next +/// Pure reducer for chat state. +public func chatReducer(state: ChatState, action: StateAction) -> ChatState { + switch action { // ── Turn Lifecycle ──────────────────────────────────────────────────── - case .sessionTurnStarted(let a): + case .chatTurnStarted(let a): var next = state - next.summary.modifiedAt = currentTimestamp() + next.modifiedAt = currentTimestamp() next.activeTurn = ActiveTurn( id: a.turnId, message: a.message, responseParts: [], usage: nil ) - // If auto-started from a pending message, remove it if let queuedId = a.queuedMessageId { if next.steeringMessage?.id == queuedId { next.steeringMessage = nil @@ -104,17 +96,17 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.queuedMessages = queued.isEmpty ? nil : queued } } - next.summary.status = withStatusFlag(sessionSummaryStatus(next), .isRead, false) + next.status = withStatusFlag(chatSummaryStatus(next), .isRead, false) return next - case .sessionDelta(let a): + case .chatDelta(let a): return updateResponsePart(state: state, turnId: a.turnId, partId: a.partId) { part in guard case .markdown(var md) = part else { return part } md.content += a.content return .markdown(md) } - case .sessionResponsePart(let a): + case .chatResponsePart(let a): guard var activeTurn = state.activeTurn, activeTurn.id == a.turnId else { return state } @@ -123,18 +115,18 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.activeTurn = activeTurn return next - case .sessionTurnComplete(let a): + case .chatTurnComplete(let a): return endTurn(state: state, turnId: a.turnId, turnState: .complete) - case .sessionTurnCancelled(let a): + case .chatTurnCancelled(let a): return endTurn(state: state, turnId: a.turnId, turnState: .cancelled) - case .sessionError(let a): + case .chatError(let a): return endTurn(state: state, turnId: a.turnId, turnState: .error, terminalStatus: .error, error: a.error) // ── Tool Call State Machine ─────────────────────────────────────────── - case .sessionToolCallStart(let a): + case .chatToolCallStart(let a): guard var activeTurn = state.activeTurn, activeTurn.id == a.turnId else { return state } @@ -154,20 +146,19 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.activeTurn = activeTurn return next - case .sessionToolCallDelta(let a): + case .chatToolCallDelta(let a): return updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .streaming(var s) = tc else { return tc } - s.meta = a.meta ?? s.meta s.partialInput = (s.partialInput ?? "") + a.content if let msg = a.invocationMessage { s.invocationMessage = msg } + s.meta = a.meta ?? s.meta return .streaming(s) } - case .sessionToolCallReady(let a): - return refreshSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - // Only process if currently streaming or running (matches TS behavior) + case .chatToolCallReady(let a): + return refreshChatSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in switch tc { case .streaming, .running: break default: return tc @@ -203,8 +194,8 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS )) }) - case .sessionToolCallConfirmed(let a): - return refreshSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in + case .chatToolCallConfirmed(let a): + return refreshChatSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .pendingConfirmation(let pending) = tc else { return tc } let base = tc.baseFields let meta = a.meta ?? base.meta @@ -239,8 +230,8 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS )) }) - case .sessionToolCallComplete(let a): - return refreshSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in + case .chatToolCallComplete(let a): + return refreshChatSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in let base = tc.baseFields let meta = a.meta ?? base.meta let confirmed: ToolCallConfirmationReason @@ -300,8 +291,8 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS )) }) - case .sessionToolCallResultConfirmed(let a): - return refreshSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in + case .chatToolCallResultConfirmed(let a): + return refreshChatSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .pendingResultConfirmation(let prc) = tc else { return tc } let base = tc.baseFields let meta = a.meta ?? base.meta @@ -338,15 +329,15 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS )) }) - // ── Metadata ────────────────────────────────────────────────────────── - - case .sessionTitleChanged(let a): - var next = state - next.summary.title = a.title - next.summary.modifiedAt = currentTimestamp() - return next + case .chatToolCallContentChanged(let a): + return updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in + guard case .running(var r) = tc else { return tc } + r.meta = a.meta ?? r.meta + r.content = a.content + return .running(r) + } - case .sessionUsage(let a): + case .chatUsage(let a): guard var activeTurn = state.activeTurn, activeTurn.id == a.turnId else { return state } @@ -355,23 +346,202 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.activeTurn = activeTurn return next - case .sessionReasoning(let a): + case .chatReasoning(let a): return updateResponsePart(state: state, turnId: a.turnId, partId: a.partId) { part in guard case .reasoning(var r) = part else { return part } r.content += a.content return .reasoning(r) } + // ── Truncation ──────────────────────────────────────────────────────── + + case .chatTruncated(let a): + let turns: [Turn] + if let turnId = a.turnId { + guard let idx = state.turns.firstIndex(where: { $0.id == turnId }) else { + return state + } + turns = Array(state.turns.prefix(idx + 1)) + } else { + turns = [] + } + var next = state + next.turns = turns + next.activeTurn = nil + next.inputRequests = nil + next.status = chatSummaryStatus(next) + next.modifiedAt = currentTimestamp() + return next + + // ── Session Input Requests ───────────────────────────────────────────── + + case .chatInputRequested(let a): + return upsertInputRequest(state: state, request: a.request) + + case .chatInputAnswerChanged(let a): + guard var existing = state.inputRequests, + let idx = existing.firstIndex(where: { $0.id == a.requestId }) else { + return state + } + var request = existing[idx] + var answers = request.answers ?? [:] + if let answer = a.answer { + answers[a.questionId] = answer + } else { + answers.removeValue(forKey: a.questionId) + } + request.answers = answers.isEmpty ? nil : answers + existing[idx] = request + var next = state + next.inputRequests = existing + next.modifiedAt = currentTimestamp() + return next + + case .chatInputCompleted(let a): + guard var existing = state.inputRequests, + existing.contains(where: { $0.id == a.requestId }) else { + return state + } + existing.removeAll { $0.id == a.requestId } + var next = state + next.inputRequests = existing.isEmpty ? nil : existing + next.status = chatSummaryStatus(next) + next.modifiedAt = currentTimestamp() + return next + + // ── Pending Messages ────────────────────────────────────────────────── + + case .chatPendingMessageSet(let a): + let entry = PendingMessage(id: a.id, message: a.message) + var next = state + if a.kind == .steering { + next.steeringMessage = entry + return next + } + var existing = next.queuedMessages ?? [] + if let idx = existing.firstIndex(where: { $0.id == a.id }) { + existing[idx] = entry + } else { + existing.append(entry) + } + next.queuedMessages = existing + return next + + case .chatPendingMessageRemoved(let a): + var next = state + if a.kind == .steering { + guard next.steeringMessage?.id == a.id else { return state } + next.steeringMessage = nil + return next + } + guard var existing = next.queuedMessages else { return state } + let before = existing.count + existing.removeAll { $0.id == a.id } + guard existing.count != before else { return state } + next.queuedMessages = existing.isEmpty ? nil : existing + return next + + case .chatQueuedMessagesReordered(let a): + guard let existing = state.queuedMessages else { return state } + let byId = Dictionary(uniqueKeysWithValues: existing.map { ($0.id, $0) }) + var ordered = Set() + var reordered: [PendingMessage] = a.order.compactMap { id in + guard let msg = byId[id], !ordered.contains(id) else { return nil } + ordered.insert(id) + return msg + } + for m in existing where !ordered.contains(m.id) { + reordered.append(m) + } + var next = state + next.queuedMessages = reordered + return next + + default: + return state + } +} + +// MARK: - Session Reducer + +/// Pure reducer for session state. +public func sessionReducer(state: SessionState, action: StateAction) -> SessionState { + switch action { + + // ── Lifecycle ────────────────────────────────────────────────────────── + + case .sessionReady: + var next = state + next.lifecycle = .ready + return next + + case .sessionCreationFailed(let a): + var next = state + next.lifecycle = .creationFailed + next.creationError = a.error + return next + + case .sessionChatAdded(let a): + var next = state + if let idx = next.chats.firstIndex(where: { $0.resource == a.summary.resource }) { + next.chats[idx] = a.summary + } else { + next.chats.append(a.summary) + } + return next + + case .sessionChatRemoved(let a): + guard let idx = state.chats.firstIndex(where: { $0.resource == a.chat }) else { + return state + } + var next = state + next.chats.remove(at: idx) + if next.defaultChat == a.chat { + next.defaultChat = nil + } + return next + + case .sessionChatUpdated(let a): + guard let idx = state.chats.firstIndex(where: { $0.resource == a.chat }) else { + return state + } + var next = state + mergeChatSummaryChanges(&next.chats[idx], changes: a.changes) + return next + + case .sessionDefaultChatChanged(let a): + var next = state + next.defaultChat = a.defaultChat + return next + + // ── Metadata ────────────────────────────────────────────────────────── + + case .sessionTitleChanged(let a): + var next = state + next.summary.title = a.title + next.summary.modifiedAt = currentTimestampMillis() + return next + case .sessionModelChanged(let a): var next = state next.summary.model = a.model - next.summary.modifiedAt = currentTimestamp() + next.summary.modifiedAt = currentTimestampMillis() return next case .sessionAgentChanged(let a): var next = state next.summary.agent = a.agent - next.summary.modifiedAt = currentTimestamp() + next.summary.modifiedAt = currentTimestampMillis() + return next + + case .sessionIsReadChanged(let a): + var next = state + next.summary.status = withStatusFlag(next.summary.status, .isRead, a.isRead) + return next + + case .sessionIsArchivedChanged(let a): + var next = state + next.summary.status = withStatusFlag(next.summary.status, .isArchived, a.isArchived) return next case .sessionActivityChanged(let a): @@ -389,7 +559,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS config.values = a.replace == true ? a.config : config.values.merging(a.config) { _, new in new } var next = state next.config = config - next.summary.modifiedAt = currentTimestamp() + next.summary.modifiedAt = currentTimestampMillis() return next case .sessionMetaChanged(let a): @@ -461,7 +631,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS } return state - case .sessionMcpServerStatusChanged(let a): + case .sessionMcpServerStateChanged(let a): guard var list = state.customizations else { return state } if let topIdx = list.firstIndex(where: { customizationId($0) == a.id }) { guard case .mcpServer(var entry) = list[topIdx] else { return state } @@ -472,7 +642,6 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS next.customizations = list return next } - var changed = false for containerIdx in list.indices { var container = list[containerIdx] guard var children = customizationChildren(container) else { continue } @@ -483,141 +652,11 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS children[childIdx] = .mcpServer(child) setCustomizationChildren(&container, children) list[containerIdx] = container - changed = true - break - } - guard changed else { return state } - var next = state - next.customizations = list - return next - - // ── Truncation ──────────────────────────────────────────────────────── - - case .sessionTruncated(let a): - let turns: [Turn] - if let turnId = a.turnId { - guard let idx = state.turns.firstIndex(where: { $0.id == turnId }) else { - return state - } - turns = Array(state.turns.prefix(idx + 1)) - } else { - turns = [] - } - var next = state - next.turns = turns - next.activeTurn = nil - next.inputRequests = nil - next.summary.status = sessionSummaryStatus(next) - next.summary.modifiedAt = currentTimestamp() - return next - - // ── Read / Archived ───────────────────────────────────────────────── - - case .sessionIsReadChanged(let a): - var next = state - next.summary.status = withStatusFlag(next.summary.status, .isRead, a.isRead) - return next - - case .sessionIsArchivedChanged(let a): - var next = state - next.summary.status = withStatusFlag(next.summary.status, .isArchived, a.isArchived) - return next - - - // ── Tool Call Content ──────────────────────────────────────────────── - - case .sessionToolCallContentChanged(let a): - return updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in - guard case .running(var r) = tc else { return tc } - r.meta = a.meta ?? r.meta - r.content = a.content - return .running(r) - } - - // ── Pending Messages ────────────────────────────────────────────────── - - case .sessionPendingMessageSet(let a): - let entry = PendingMessage(id: a.id, message: a.message) - var next = state - if a.kind == .steering { - next.steeringMessage = entry - return next - } - var existing = next.queuedMessages ?? [] - if let idx = existing.firstIndex(where: { $0.id == a.id }) { - existing[idx] = entry - } else { - existing.append(entry) - } - next.queuedMessages = existing - return next - - case .sessionPendingMessageRemoved(let a): - var next = state - if a.kind == .steering { - guard next.steeringMessage?.id == a.id else { return state } - next.steeringMessage = nil + var next = state + next.customizations = list return next } - guard var existing = next.queuedMessages else { return state } - let before = existing.count - existing.removeAll { $0.id == a.id } - guard existing.count != before else { return state } - next.queuedMessages = existing.isEmpty ? nil : existing - return next - - case .sessionQueuedMessagesReordered(let a): - guard let existing = state.queuedMessages else { return state } - let byId = Dictionary(uniqueKeysWithValues: existing.map { ($0.id, $0) }) - var ordered = Set() - var reordered: [PendingMessage] = a.order.compactMap { id in - guard let msg = byId[id], !ordered.contains(id) else { return nil } - ordered.insert(id) - return msg - } - // Append any messages not in the new order - for m in existing where !ordered.contains(m.id) { - reordered.append(m) - } - var next = state - next.queuedMessages = reordered - return next - - // ── Session Input Requests ───────────────────────────────────────────── - - case .sessionInputRequested(let a): - return upsertInputRequest(state: state, request: a.request) - - case .sessionInputAnswerChanged(let a): - guard var existing = state.inputRequests, - let idx = existing.firstIndex(where: { $0.id == a.requestId }) else { - return state - } - var request = existing[idx] - var answers = request.answers ?? [:] - if let answer = a.answer { - answers[a.questionId] = answer - } else { - answers.removeValue(forKey: a.questionId) - } - request.answers = answers.isEmpty ? nil : answers - existing[idx] = request - var next = state - next.inputRequests = existing - next.summary.modifiedAt = currentTimestamp() - return next - - case .sessionInputCompleted(let a): - guard var existing = state.inputRequests, - existing.contains(where: { $0.id == a.requestId }) else { - return state - } - existing.removeAll { $0.id == a.requestId } - var next = state - next.inputRequests = existing.isEmpty ? nil : existing - next.summary.status = sessionSummaryStatus(next) - next.summary.modifiedAt = currentTimestamp() - return next + return state default: return state @@ -628,19 +667,20 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS /// Set of action types that clients are allowed to dispatch. public let clientDispatchableActions: Set = [ - "session/turnStarted", - "session/toolCallConfirmed", - "session/toolCallComplete", - "session/toolCallResultConfirmed", - "session/turnCancelled", + "chat/turnStarted", + "chat/toolCallConfirmed", + "chat/toolCallComplete", + "chat/toolCallResultConfirmed", + "chat/turnCancelled", "session/modelChanged", + "session/agentChanged", "session/activeClientChanged", "session/activeClientToolsChanged", - "session/pendingMessageSet", - "session/pendingMessageRemoved", - "session/queuedMessagesReordered", - "session/inputAnswerChanged", - "session/inputCompleted", + "chat/pendingMessageSet", + "chat/pendingMessageRemoved", + "chat/queuedMessagesReordered", + "chat/inputAnswerChanged", + "chat/inputCompleted", "session/customizationToggled", "session/isReadChanged", "session/isArchivedChanged", @@ -649,12 +689,12 @@ public let clientDispatchableActions: Set = [ /// Checks whether an action may be dispatched by a client. public func isClientDispatchable(_ action: StateAction) -> Bool { switch action { - case .sessionTurnStarted, .sessionToolCallConfirmed, .sessionToolCallComplete, - .sessionToolCallResultConfirmed, .sessionTurnCancelled, - .sessionModelChanged, .sessionActiveClientChanged, - .sessionActiveClientToolsChanged, .sessionPendingMessageSet, - .sessionPendingMessageRemoved, .sessionQueuedMessagesReordered, - .sessionInputAnswerChanged, .sessionInputCompleted, + case .chatTurnStarted, .chatToolCallConfirmed, .chatToolCallComplete, + .chatToolCallResultConfirmed, .chatTurnCancelled, + .sessionModelChanged, .sessionAgentChanged, .sessionActiveClientChanged, + .sessionActiveClientToolsChanged, .chatPendingMessageSet, + .chatPendingMessageRemoved, .chatQueuedMessagesReordered, + .chatInputAnswerChanged, .chatInputCompleted, .sessionCustomizationToggled, .sessionIsReadChanged, .sessionIsArchivedChanged: return true @@ -665,11 +705,27 @@ public func isClientDispatchable(_ action: StateAction) -> Bool { // MARK: - Helpers -private func currentTimestamp() -> Int { +private func currentTimestampMillis() -> Int { currentTimestampProvider() } -private func sessionSummaryStatus(_ state: SessionState, terminalStatus: SessionStatus? = nil) -> SessionStatus { +private func currentTimestamp() -> String { + let date = Date(timeIntervalSince1970: Double(currentTimestampProvider()) / 1000) + return iso8601TimestampFormatter.string(from: date) +} + +private func mergeChatSummaryChanges(_ summary: inout ChatSummary, changes: PartialChatSummary) { + if let title = changes.title { summary.title = title } + if let status = changes.status { summary.status = status } + if let activity = changes.activity { summary.activity = activity } + if let modifiedAt = changes.modifiedAt { summary.modifiedAt = modifiedAt } + if let model = changes.model { summary.model = model } + if let agent = changes.agent { summary.agent = agent } + if let origin = changes.origin { summary.origin = origin } + if let workingDirectory = changes.workingDirectory { summary.workingDirectory = workingDirectory } +} + +private func chatSummaryStatus(_ state: ChatState, terminalStatus: SessionStatus? = nil) -> SessionStatus { let activity: SessionStatus if let terminalStatus { activity = terminalStatus @@ -680,11 +736,11 @@ private func sessionSummaryStatus(_ state: SessionState, terminalStatus: Session } else { activity = .idle } - return state.summary.status.subtracting(statusActivityMask).union(activity) + return state.status.subtracting(statusActivityMask).union(activity) } /// Returns `true` if the active turn has any tool call awaiting user confirmation. -private func hasPendingToolCallConfirmation(_ state: SessionState) -> Bool { +private func hasPendingToolCallConfirmation(_ state: ChatState) -> Bool { guard let activeTurn = state.activeTurn else { return false } for part in activeTurn.responseParts { guard case .toolCall(let tcPart) = part else { continue } @@ -698,18 +754,15 @@ private func hasPendingToolCallConfirmation(_ state: SessionState) -> Bool { return false } -/// Returns a state with `summary.status` recomputed. Use this after reducers -/// that change data feeding into `sessionSummaryStatus` (e.g. tool call -/// lifecycle transitions that may enter or leave a pending-confirmation state). -private func refreshSummaryStatus(_ state: SessionState) -> SessionState { - let status = sessionSummaryStatus(state) - guard status != state.summary.status else { return state } +private func refreshChatSummaryStatus(_ state: ChatState) -> ChatState { + let status = chatSummaryStatus(state) + guard status != state.status else { return state } var next = state - next.summary.status = status + next.status = status return next } -private func upsertInputRequest(state: SessionState, request: SessionInputRequest) -> SessionState { +private func upsertInputRequest(state: ChatState, request: ChatInputRequest) -> ChatState { var next = state var existing = next.inputRequests ?? [] if let idx = existing.firstIndex(where: { $0.id == request.id }) { @@ -720,23 +773,20 @@ private func upsertInputRequest(state: SessionState, request: SessionInputReques existing.append(request) } next.inputRequests = existing - next.summary.status = withStatusFlag(sessionSummaryStatus(next), .isRead, false) - next.summary.modifiedAt = currentTimestamp() + next.status = withStatusFlag(chatSummaryStatus(next), .isRead, false) + next.modifiedAt = currentTimestamp() return next } -// ToolCallBaseFields and toolCallBase() are now shared via -// ToolCallState.baseFields in ToolCallStateExtensions.swift. - /// Ends the active turn, producing a completed Turn record. /// Non-terminal tool calls are forced to cancelled. private func endTurn( - state: SessionState, + state: ChatState, turnId: String, turnState: TurnState, terminalStatus: SessionStatus? = nil, error: ErrorInfo? = nil -) -> SessionState { +) -> ChatState { guard let activeTurn = state.activeTurn, activeTurn.id == turnId else { return state } @@ -798,18 +848,18 @@ private func endTurn( next.turns.append(turn) next.activeTurn = nil next.inputRequests = nil - next.summary.status = sessionSummaryStatus(next, terminalStatus: terminalStatus) - next.summary.modifiedAt = currentTimestamp() + next.status = chatSummaryStatus(next, terminalStatus: terminalStatus) + next.modifiedAt = currentTimestamp() return next } /// Updates a tool call inside the active turn's response parts. private func updateToolCall( - state: SessionState, + state: ChatState, turnId: String, toolCallId: String, updater: (ToolCallState) -> ToolCallState -) -> SessionState { +) -> ChatState { guard var activeTurn = state.activeTurn, activeTurn.id == turnId else { return state } @@ -832,11 +882,11 @@ private func updateToolCall( /// Updates a response part identified by partId in the active turn. private func updateResponsePart( - state: SessionState, + state: ChatState, turnId: String, partId: String, updater: (ResponsePart) -> ResponsePart -) -> SessionState { +) -> ChatState { guard var activeTurn = state.activeTurn, activeTurn.id == turnId else { return state } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift index e61e0a19..7c44fb33 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift @@ -13,6 +13,7 @@ import AgentHostProtocol public actor AHPStateMirror { public private(set) var rootState: RootState = RootState(agents: []) public private(set) var sessions: [String: SessionState] = [:] + public private(set) var chats: [String: ChatState] = [:] public private(set) var terminals: [String: TerminalState] = [:] public private(set) var changesets: [String: ChangesetState] = [:] public private(set) var annotations: [String: AnnotationsState] = [:] @@ -33,16 +34,19 @@ public actor AHPStateMirror { rootState = rootReducer(state: rootState, action: action) return } - if var session = sessions[channel] { + if channel.hasPrefix("ahp-session:"), var session = sessions[channel] { session = sessionReducer(state: session, action: action) sessions[channel] = session return } - if terminals[channel] != nil { - // Terminals don't have a hand-written reducer in the Swift - // package today; just leave the slot as the latest snapshot. - // (Native reducer + state shape will be wired up when - // terminal lifecycle reducers ship.) + if channel.hasPrefix("ahp-chat:"), var chat = chats[channel] { + chat = chatReducer(state: chat, action: action) + chats[channel] = chat + return + } + if channel.hasPrefix("ahp-terminal:"), var terminal = terminals[channel] { + terminal = terminalReducer(state: terminal, action: action) + terminals[channel] = terminal return } if changesets[channel] != nil { @@ -72,6 +76,8 @@ public actor AHPStateMirror { rootState = state case .session(let state): sessions[snapshot.resource] = state + case .chat(let state): + chats[snapshot.resource] = state case .terminal(let state): terminals[snapshot.resource] = state case .changeset(let state): @@ -87,6 +93,7 @@ public actor AHPStateMirror { public func reset() { rootState = RootState(agents: []) sessions.removeAll() + chats.removeAll() terminals.removeAll() changesets.removeAll() annotations.removeAll() diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift index 2ab48ddd..767b7ef2 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift @@ -45,6 +45,7 @@ public struct HostedResourceKey: Hashable, Sendable { public actor MultiHostStateMirror { public private(set) var rootStates: [HostId: RootState] = [:] public private(set) var sessions: [HostedResourceKey: SessionState] = [:] + public private(set) var chats: [HostedResourceKey: ChatState] = [:] public private(set) var terminals: [HostedResourceKey: TerminalState] = [:] public private(set) var changesets: [HostedResourceKey: ChangesetState] = [:] public private(set) var annotations: [HostedResourceKey: AnnotationsState] = [:] @@ -74,14 +75,19 @@ public actor MultiHostStateMirror { return } let key = HostedResourceKey(hostId: host, uri: channel) - if var session = sessions[key] { + if channel.hasPrefix("ahp-session:"), var session = sessions[key] { session = sessionReducer(state: session, action: action) sessions[key] = session return } - if terminals[key] != nil { - // Terminals don't have a hand-written reducer in the Swift - // package today; just leave the slot as the latest snapshot. + if channel.hasPrefix("ahp-chat:"), var chat = chats[key] { + chat = chatReducer(state: chat, action: action) + chats[key] = chat + return + } + if channel.hasPrefix("ahp-terminal:"), var terminal = terminals[key] { + terminal = terminalReducer(state: terminal, action: action) + terminals[key] = terminal return } if changesets[key] != nil { @@ -114,6 +120,8 @@ public actor MultiHostStateMirror { rootStates[host] = state case .session(let state): sessions[key] = state + case .chat(let state): + chats[key] = state case .terminal(let state): terminals[key] = state case .changeset(let state): @@ -132,6 +140,7 @@ public actor MultiHostStateMirror { public func reset(host: HostId) { rootStates.removeValue(forKey: host) sessions = sessions.filter { $0.key.hostId != host } + chats = chats.filter { $0.key.hostId != host } terminals = terminals.filter { $0.key.hostId != host } changesets = changesets.filter { $0.key.hostId != host } annotations = annotations.filter { $0.key.hostId != host } @@ -142,6 +151,7 @@ public actor MultiHostStateMirror { public func reset() { rootStates.removeAll() sessions.removeAll() + chats.removeAll() terminals.removeAll() changesets.removeAll() annotations.removeAll() diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift index 583d19e6..35561a82 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift @@ -58,7 +58,7 @@ final class AHPClientTests: XCTestCase { createdAt: 1, modifiedAt: 1 ), lifecycle: .ready, - turns: [] + chats: [] )), fromSeq: 0 )) diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPStateMirrorTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPStateMirrorTests.swift index f294d303..9ba13d1a 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPStateMirrorTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPStateMirrorTests.swift @@ -33,7 +33,7 @@ final class AHPStateMirrorTests: XCTestCase { createdAt: 1, modifiedAt: 1 ), lifecycle: .ready, - turns: [] + chats: [] ) let snapshot = Snapshot( resource: "ahp-session:/s1", @@ -75,7 +75,7 @@ final class AHPStateMirrorTests: XCTestCase { createdAt: 1, modifiedAt: 1 ), lifecycle: .ready, - turns: [] + chats: [] ) await mirror.applySnapshot(Snapshot( resource: "ahp-session:/s1", diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift index 9de46c40..1d9c8568 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift @@ -39,14 +39,14 @@ final class MultiHostStateMirrorTests: XCTestCase { resource: "ahp-session:/s1", provider: "x", title: "A title", status: .idle, createdAt: 1, modifiedAt: 1 ), - lifecycle: .ready, turns: [] + lifecycle: .ready, chats: [] ) let sessionB = SessionState( summary: SessionSummary( resource: "ahp-session:/s1", provider: "x", title: "B title", status: .idle, createdAt: 1, modifiedAt: 1 ), - lifecycle: .ready, turns: [] + lifecycle: .ready, chats: [] ) await mirror.applySnapshot( @@ -101,7 +101,7 @@ final class MultiHostStateMirrorTests: XCTestCase { resource: "ahp-session:/s1", provider: "x", title: "Old", status: .idle, createdAt: 1, modifiedAt: 1 ), - lifecycle: .ready, turns: [] + lifecycle: .ready, chats: [] ) await mirror.applySnapshot( host: "alpha", diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift index 27665ba1..3d2dffd3 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift @@ -225,6 +225,10 @@ final class FixtureDrivenReducerTests: XCTestCase { try compareFixture(file: file, fixture: fixture, stateType: ResourceWatchState.self) { state in actions.reduce(state) { resourceWatchReducer(state: $0, action: $1) } } + case "chat": + try compareFixture(file: file, fixture: fixture, stateType: ChatState.self) { state in + actions.reduce(state) { chatReducer(state: $0, action: $1) } + } default: throw FixtureError.unsupportedReducer(fixture.reducer) } diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift index 46d3e555..0a81e24a 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift @@ -15,13 +15,15 @@ final class NativeReducerTests: XCTestCase { // MARK: - Constants - private let S = "copilot:/test-session" + private let S = "ahp-session:/test-session" + private let C = "ahp-chat:/test-session/default" private let T = "turn-1" // MARK: - Reducers under test private let rootR = AHPRootReducer() private let sessionR = AHPSessionReducer() + private let chatR = AHPChatReducer() // MARK: - Fixtures @@ -39,21 +41,16 @@ final class NativeReducerTests: XCTestCase { modifiedAt: 1000 ), lifecycle: lifecycle, - turns: [] + chats: [] ) } - private func makeSessionStateWithActiveTurn() -> SessionState { - SessionState( - summary: SessionSummary( - resource: S, - provider: "copilot", - title: "Test Session", - status: .inProgress, - createdAt: 1000, - modifiedAt: 2000 - ), - lifecycle: .ready, + private func makeChatStateWithActiveTurn() -> ChatState { + ChatState( + resource: C, + title: "Test Chat", + status: .inProgress, + modifiedAt: "1970-01-01T00:00:02.000Z", turns: [], activeTurn: ActiveTurn( id: T, @@ -69,6 +66,7 @@ final class NativeReducerTests: XCTestCase { func testReducerProtocolConformance() { let _: any Reducer = AHPRootReducer() let _: any Reducer = AHPSessionReducer() + let _: any Reducer = AHPChatReducer() } func testTypeErasure() { @@ -124,17 +122,17 @@ final class NativeReducerTests: XCTestCase { } func testInoutMutationEfficiency() { - var state = makeSessionStateWithActiveTurn() + var state = makeChatStateWithActiveTurn() - sessionR.reduce(into: &state, action: .sessionResponsePart(SessionResponsePartAction( - type: .sessionResponsePart, turnId: T, + chatR.reduce(into: &state, action: .chatResponsePart(ChatResponsePartAction( + type: .chatResponsePart, turnId: T, part: .markdown(MarkdownResponsePart(kind: .markdown, id: "md-1", content: "")) ))) - sessionR.reduce(into: &state, action: .sessionDelta(SessionDeltaAction( - type: .sessionDelta, turnId: T, partId: "md-1", content: "Hello" + chatR.reduce(into: &state, action: .chatDelta(ChatDeltaAction( + type: .chatDelta, turnId: T, partId: "md-1", content: "Hello" ))) - sessionR.reduce(into: &state, action: .sessionDelta(SessionDeltaAction( - type: .sessionDelta, turnId: T, partId: "md-1", content: " World" + chatR.reduce(into: &state, action: .chatDelta(ChatDeltaAction( + type: .chatDelta, turnId: T, partId: "md-1", content: " World" ))) let text = state.activeTurn?.responseParts.compactMap { part in diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift index a4583c98..5b8207be 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift @@ -13,7 +13,8 @@ final class ReducersTests: XCTestCase { // MARK: - Constants - private let S = "copilot:/test-session" + private let S = "ahp-session:/test-session" + private let C = "ahp-chat:/test-session/default" private let T = "turn-1" // MARK: - Fixtures @@ -36,21 +37,16 @@ final class ReducersTests: XCTestCase { modifiedAt: 1000 ), lifecycle: lifecycle, - turns: [] + chats: [] ) } - private func makeSessionStateWithActiveTurn() -> SessionState { - SessionState( - summary: SessionSummary( - resource: S, - provider: "copilot", - title: "Test Session", - status: .inProgress, - createdAt: 1000, - modifiedAt: 2000 - ), - lifecycle: .ready, + private func makeChatStateWithActiveTurn() -> ChatState { + ChatState( + resource: C, + title: "Test Chat", + status: .inProgress, + modifiedAt: "1970-01-01T00:00:02.000Z", turns: [], activeTurn: ActiveTurn( id: T, @@ -73,26 +69,21 @@ final class ReducersTests: XCTestCase { XCTAssertEqual(state.agents.count, 0) } - func testSessionReducerDoesNotMutateTurnsArray() { + func testChatReducerDoesNotMutateTurnsArray() { let turn1 = Turn(id: "t1", message: Message(text: "First", origin: AnyCodable(["kind": "user"])), responseParts: [], state: .complete) let turn2 = Turn(id: "t2", message: Message(text: "Second", origin: AnyCodable(["kind": "user"])), responseParts: [], state: .complete) let turn3 = Turn(id: "t3", message: Message(text: "Third", origin: AnyCodable(["kind": "user"])), responseParts: [], state: .complete) - let state = SessionState( - summary: SessionSummary( - resource: S, - provider: "copilot", - title: "T", - status: .idle, - createdAt: 1000, - modifiedAt: 1000 - ), - lifecycle: .ready, + let state = ChatState( + resource: C, + title: "T", + status: .idle, + modifiedAt: "1970-01-01T00:00:01.000Z", turns: [turn1, turn2, turn3] ) let original = state.turns - _ = sessionReducer( + _ = chatReducer( state: state, - action: .sessionTruncated(SessionTruncatedAction(type: .sessionTruncated, turnId: "t1")) + action: .chatTruncated(ChatTruncatedAction(type: .chatTruncated, turnId: "t1")) ) XCTAssertEqual(state.turns.count, original.count) } @@ -100,8 +91,8 @@ final class ReducersTests: XCTestCase { // MARK: - Dispatch Validation func testClientDispatchableReturnsTrue() { - let action: StateAction = .sessionTurnStarted(SessionTurnStartedAction( - type: .sessionTurnStarted, turnId: T, message: Message(text: "Hello", origin: AnyCodable(["kind": "user"])) + let action: StateAction = .chatTurnStarted(ChatTurnStartedAction( + type: .chatTurnStarted, turnId: T, message: Message(text: "Hello", origin: AnyCodable(["kind": "user"])) )) XCTAssertTrue(isClientDispatchable(action)) } @@ -113,15 +104,21 @@ final class ReducersTests: XCTestCase { // MARK: - Timestamp Behavior - func testTurnStartedUpdatesModifiedAt() { - let state = makeSessionState(lifecycle: .ready) - let next = sessionReducer( + func testChatTurnStartedUpdatesModifiedAt() { + let state = ChatState( + resource: C, + title: "Test Chat", + status: .idle, + modifiedAt: "1970-01-01T00:00:01.000Z", + turns: [] + ) + let next = chatReducer( state: state, - action: .sessionTurnStarted(SessionTurnStartedAction( - type: .sessionTurnStarted, turnId: T, message: Message(text: "Hello", origin: AnyCodable(["kind": "user"])) + action: .chatTurnStarted(ChatTurnStartedAction( + type: .chatTurnStarted, turnId: T, message: Message(text: "Hello", origin: AnyCodable(["kind": "user"])) )) ) - XCTAssertGreaterThan(next.summary.modifiedAt, state.summary.modifiedAt) + XCTAssertGreaterThan(next.modifiedAt, state.modifiedAt) } func testTitleChangedUpdatesModifiedAt() { diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index f53cc391..6e5efa08 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -38,8 +38,9 @@ the tag matches the version pinned in [`VERSION`](VERSION). resending its entries. Handled by the annotations reducer (no-op on unknown id). -### Added - +- `ahp-chat:` channel for per-chat conversation state; `SessionState.chats[]` catalog; `SessionState.defaultChat?` input-routing hint; `ChatOrigin` provenance union; `createChat` command. +- `SessionChatAddedAction`, `SessionChatRemovedAction`, and `SessionChatUpdatedAction` handling for incremental chat catalog updates. +- `ChatSummary.workingDirectory` — optional per-chat working directory. Falls back to the session's `workingDirectory` when absent. - `RootState` now exposes an optional `_meta` property bag (`meta: [String: AnyCodable]?`) for implementation-defined agent-host metadata, such as a well-known `hostBuild` key carrying the host's build version/commit/date. @@ -53,6 +54,15 @@ the tag matches the version pinned in [`VERSION`](VERSION). remaining gaps (unknown-discriminant response part; the not-yet-implemented annotations channel) pinned by an explicit drift tripwire. +### Changed + +- `ChatState` is now flat — the previous embedded `summary` has been replaced with inlined `resource` / `title` / `status` / `activity` / `modifiedAt` / `model` / `agent` / `origin` / `workingDirectory` properties. `ChatSummary` remains as the standalone catalog entry on `SessionState.chats`. +- `ChatSummary.modifiedAt` and `ChatState.modifiedAt` are now ISO 8601 `String` values instead of `Int64`/`UInt64` milliseconds. + +### Removed + +- `SessionChatsChangedAction` (replaced by the three discrete chat-catalog actions above). + ## [0.3.0] — 2026-06-05 Implements AHP 0.3.0. @@ -98,11 +108,13 @@ Implements AHP 0.3.0. ### Changed +- `fetchTurns` and `completions` now target an `ahp-chat:` channel; `PROTOCOL_VERSION` bumped to `0.4.0`. - Renamed the `ChangesetSummary` type to `Changeset`. The on-the-wire shape is unchanged. - Moved the `changesets` catalogue from `SessionSummary` to `SessionState`. The `session/changesetsChanged` action now updates `state.changesets` directly instead of `state.summary.changesets`. ### Removed +- `SessionState.turns`, `SessionState.activeTurn`, `SessionState.steeringMessage`, `SessionState.queuedMessages`, `SessionState.inputRequests` (moved to `ChatState`). - Removed the `additions`, `deletions`, and `files` fields from `ChangesetSummary`. Aggregate counts now live on `SessionSummary.changes`; per-changeset views derive their own totals from `ChangesetState.files`. ### Changed diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 14a92501..d359e670 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -38,12 +38,22 @@ hotfix escape hatch. existing annotation's `turnId` / `resource` / `range` / `resolved` without resending its entries. Handled by `annotationsReducer` (no-op on unknown id). -### Added - +- `ahp-chat:` channel for per-chat conversation state; `SessionState.chats[]` catalog; `SessionState.defaultChat?` input-routing hint; `ChatOrigin` provenance union; `createChat` command. +- `ChatSummary.workingDirectory?` — optional per-chat working directory. Falls back to the session's `workingDirectory` when absent. +- Three discrete chat-catalog actions on the session channel — `SessionChatAddedAction` (upsert by `summary.resource`), `SessionChatRemovedAction`, and `SessionChatUpdatedAction` (partial-update with `Partial`). - `RootState` now exposes an optional `_meta` property bag (`_meta?: Record`) for implementation-defined agent-host metadata, such as a well-known `hostBuild` key carrying the host's build version/commit/date. +### Changed + +- `ChatState` is now flat — the previous nested `summary: ChatSummary` has been replaced with inlined `resource` / `title` / `status` / `activity` / `modifiedAt` / `model` / `agent` / `origin` / `workingDirectory` fields. `ChatSummary` remains as the standalone catalog entry on `SessionState.chats`. +- `ChatSummary.modifiedAt` and `ChatState.modifiedAt` are now ISO 8601 `string` values instead of numeric milliseconds. + +### Removed + +- `SessionChatsChangedAction` (replaced by the three discrete chat-catalog actions above). + ## [0.3.0] — 2026-06-05 Implements AHP 0.3.0. @@ -88,11 +98,13 @@ Implements AHP 0.3.0. ### Changed +- `fetchTurns` and `completions` now target an `ahp-chat:` channel; `PROTOCOL_VERSION` bumped to `0.4.0`. - Renamed the `ChangesetSummary` type to `Changeset`. The on-the-wire shape is unchanged. - Moved the `changesets` catalogue from `SessionSummary` to `SessionState`. The `session/changesetsChanged` action now updates `state.changesets` directly instead of `state.summary.changesets`. ### Removed +- `SessionState.turns`, `SessionState.activeTurn`, `SessionState.steeringMessage`, `SessionState.queuedMessages`, `SessionState.inputRequests` (moved to `ChatState`). - Removed the `additions`, `deletions`, and `files` fields from `ChangesetSummary`. Aggregate counts now live on `SessionSummary.changes`; per-changeset views derive their own totals from `ChangesetState.files`. ### Changed diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 6b988f5b..6de549bd 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -67,6 +67,7 @@ export default withMermaid(defineConfig({ items: [ { text: 'Root Channel', link: '/specification/root-channel' }, { text: 'Session Channel', link: '/specification/session-channel' }, + { text: 'Chat Channel', link: '/specification/chat-channel' }, { text: 'Terminal Channel', link: '/specification/terminal-channel' }, { text: 'Resource Watch Channel', link: '/specification/resource-watch-channel' }, { text: 'Telemetry Channel', link: '/specification/telemetry-channel' }, @@ -87,6 +88,7 @@ export default withMermaid(defineConfig({ items: [ { text: 'Root Channel', link: '/reference/root' }, { text: 'Session Channel', link: '/reference/session' }, + { text: 'Chat Channel', link: '/reference/chat' }, { text: 'Terminal Channel', link: '/reference/terminal' }, { text: 'Changeset Channel', link: '/reference/changeset' }, { text: 'Annotations Channel', link: '/reference/annotations' }, diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md new file mode 100644 index 00000000..03b2efb3 --- /dev/null +++ b/docs/specification/chat-channel.md @@ -0,0 +1,137 @@ +# Chat Channel + +A chat channel carries the full state of a single conversation thread: turns, streaming responses, tool calls, pending messages, and input requests. A chat always belongs to a [session](./session-channel); a session may contain one or many chats. Chats are independently subscribable so a client can observe a subset of activity without paying the bandwidth cost of every chat in the session. + +## URI + +``` +ahp-chat:/ +``` + +The path is a server-unique identifier (typically a UUID) allocated by the server when the chat is created. The owning session URI is **not** encoded in the chat URI — the relationship is expressed via the session's [`chats`](/reference/session#sessionstate) catalog and each chat's [`origin`](/reference/chat#chatorigin). + +Multiple chat channels may be active simultaneously. Clients subscribe to each chat whose state they want to track. + +## State + +Subscribers receive a [`ChatState`](/reference/chat#chatstate) snapshot. `ChatState` **denormalizes** the [`ChatSummary`](/reference/chat#chatsummary) fields directly onto itself (`resource`, `title`, `status`, `activity`, `modifiedAt`, `model`, `agent`, `origin`, `workingDirectory`) and adds the conversation contents (history of completed turns, the active turn if any, pending messages, outstanding input requests). Producers MUST keep the chat's `ChatSummary` in the session catalog consistent with these inlined fields — typically by dispatching a matching [`session/chatUpdated`](/reference/session#actions) whenever any summary field on the chat changes. Refer to the [State Model guide](/guide/state-model) for a structural overview. + +### Per-chat working directory + +`ChatState.workingDirectory` (and its mirror on [`ChatSummary`](/reference/chat#chatsummary)) is **optional**. When absent, the chat inherits the session's [`workingDirectory`](/reference/session#sessionsummary). Hosts MAY set a per-chat working directory to give individual chats their own filesystem context — for example, allocating a separate git worktree per chat so multiple chats in the same session can make independent edits that the orchestrating chat later merges back. The session-level `workingDirectory` is then the default/primary location for chats that do not override it. + +## Relationship to the session channel + +- A chat's [`ChatSummary`](/reference/chat#chatsummary) appears in the session's [`SessionState.chats`](/reference/session#sessionstate) catalog. The session reducer keeps that catalog in sync with the underlying chat lifecycle. +- The session may also expose [`defaultChat`](/reference/session#sessionstate) as a UI routing hint for input that is addressed to the session as a whole. This is advisory only — chats remain equal peers at the protocol level. +- Session-level fields such as [`status`](/reference/session#sessionsummary), `activity`, and `modifiedAt` are aggregates derived from the session's chats. See the [Session Channel specification](./session-channel#chat-aggregation) for the derivation rules. + +## Lifecycle + +``` +1. Client subscribes to the owning session URI (ahp-session:/) +2. Client (or the server, via a tool call or fork) creates a chat with createChat +3. Server allocates a chat URI (ahp-chat:/) and mutates the session's chats catalog +4. Client subscribes to the chat URI to receive its ChatState snapshot +5. Server streams chat actions over the chat channel until the chat (or its session) is disposed +``` + +### Creation + +[`createChat`](/reference/chat#createchat) is a JSON-RPC request. Callers identify the owning session via the request's `channel` parameter (`ahp-session:/`) and MAY supply: + +- an `initialMessage` to start the first turn immediately, +- per-chat `agent` / `model` / `config` overrides that win over the session defaults, and +- a `source` of type [`ChatForkSource`](/reference/chat#chatforksource) to fork from an existing chat at a specific turn. + +The server allocates the chat URI and adds the chat to the session's catalog (`session/chatAdded` on the session channel) before returning. + +### Origin + +Each chat advertises how it came into existence via [`ChatOrigin`](/reference/chat#chatorigin): + +| Kind | Meaning | +|---|---| +| `user` | User created the chat explicitly (e.g. via the host UI). | +| `fork` | Forked from an existing chat at a specific turn — payload references the source chat URI and turn id. | +| `tool` | Spawned by a tool call running in another chat — payload references the source chat URI and tool call id (e.g. a sub-agent delegation). | + +Clients MAY use the origin to render contextual UI (parent indicators, fork markers, "spawned by tool" badges), but origin is **not** a hierarchy — every chat is equally addressable. + +### Active chat + +Once a chat exists and its session is `lifecycle: 'ready'`, the chat accepts turns. The wire shape mirrors the legacy single-chat session shape: + +- The client dispatches `chat/turnStarted` to begin a turn. +- The server streams `chat/delta`, `chat/responsePart`, `chat/toolCallStart`, `chat/toolCallReady`, and related actions. +- The client dispatches `chat/toolCallConfirmed` / `chat/toolCallResultConfirmed` to approve or deny tool calls, or `chat/turnCancelled` to abort. +- The server dispatches `chat/turnComplete` or `chat/error` when the turn ends. +- The server MAY dispatch `chat/inputRequested` while a turn is active. Clients sync answer drafts with `chat/inputAnswerChanged` and finish the request with `chat/inputCompleted`. + +All actions dispatched on this channel travel on `ActionEnvelope`s whose `channel` is the chat URI. Action payloads do NOT carry their own chat URI — the channel comes from the envelope. + +### Disposal + +A chat is implicitly disposed when its owning session is disposed. The protocol does not currently expose a `disposeChat` command; chats live for the life of their session unless the server prunes them. When a chat is removed (whether explicitly or because its session was torn down), the server MUST update the session's `chats` catalog via `session/chatRemoved` so subscribers can release their per-chat subscriptions. + +## Methods and events on this channel + +This section lists wire methods that are interpreted in the context of a chat URI (`ahp-chat:/`). + +### Commands (`params.channel = "ahp-chat:/"`) + +| Method | Kind | Purpose | +|---|---|---| +| `fetchTurns` | request | Page historical turns for this chat. | +| `completions` | request | Chat-scoped inline completions (e.g. user-message mentions). | + +`createChat` is dispatched against the owning session URI (`params.channel = "ahp-session:/"`). + +### Notifications (`params.channel = "ahp-chat:/"`) + +| Method | Kind | Meaning | +|---|---|---| +| `action` | server → client notification | Chat action envelope (`chat/*` action payloads). | +| `dispatchAction` | client → server notification | Dispatch client actions on this chat (`chat/turnStarted`, `chat/toolCallConfirmed`, ...). | +| `unsubscribe` | client → server notification | Stop receiving messages for this chat channel. | + +## Server Validation of Client Actions + +When the server receives a client-dispatched action on this channel, it MUST validate it before applying. Invalid actions MUST be echoed back with a `rejectionReason` on the `ActionEnvelope`. The validation rules mirror the legacy session validation table — substitute `chat/*` for `session/*`: + +| Action | Condition | Server Behavior | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| Any action referencing a non-existent chat | Channel URI not found | Server MUST silently ignore the action (no echo) | +| `chat/toolCallConfirmed` | Tool call not in `pending-confirmation` state | Server MUST reject the action | +| `chat/turnCancelled` | No active turn | Server MUST reject the action | +| `chat/inputAnswerChanged` | No input request with matching `requestId` | Server SHOULD reject the action | +| `chat/inputAnswerChanged` | `answer.state` requires a value but `answer.value` is absent, or `answer.value.kind` is missing the matching payload field | Server SHOULD reject the action | +| `chat/inputCompleted` | No input request with matching `requestId` | Server SHOULD reject the action | +| `chat/inputCompleted` | `response` is `'accept'` but required questions do not have submitted answers | Server SHOULD reject the action | +| `chat/pendingMessageRemoved` | No pending message with matching `id` and `kind` | Server SHOULD reject the action | + +## Pending Message Consumption + +Pending messages live on the chat, not the session. The consumption rules mirror the legacy session behavior: + +### Queued Messages + +When a turn completes and `queuedMessages` is non-empty, the server SHOULD: + +1. Dispatch `chat/pendingMessageRemoved` with `kind: 'queued'` for the first queued message. +2. Dispatch `chat/turnStarted` with the queued message's `message` and `queuedMessageId` set to the message's `id`. + +When a queued message is added while the chat is idle (no active turn), the server SHOULD immediately consume it using the same two-step sequence. + +### Steering Messages + +When a turn is active and `steeringMessages` is non-empty, the server MAY consume steering messages at its discretion. To consume a steering message, the server: + +1. Dispatches `chat/pendingMessageRemoved` with `kind: 'steering'`. +2. Injects the message content into the model context (the injection mechanism is opaque to the protocol). + +Steering messages added while idle are silently stored and consumed when a turn becomes active. + +## Actions + +Refer to the [Chat Channel Reference](/reference/chat#actions) for the full per-action reference. All chat-scoped action envelopes carry `channel: "ahp-chat:/"`. diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md index 9a8c8069..0d5578ec 100644 --- a/docs/specification/session-channel.md +++ b/docs/specification/session-channel.md @@ -1,6 +1,6 @@ # Session Channel -A session channel carries the full state of a single agent conversation: turns, streaming responses, tool calls, pending messages, input requests, customizations, and per-session configuration. One session channel exists per session for as long as the session is alive. +A session channel carries session-level state and acts as the coordination scope for one or more chats. The session tracks lifecycle, model/agent defaults, customizations, per-session configuration, changesets, and the catalog of chats that belong to the session. The per-conversation state — turns, streaming responses, tool calls, pending messages, and input requests — lives on the [chat channel](./chat-channel). ## URI @@ -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, history of completed turns, the active turn (if any), pending messages, outstanding input requests, model and active-client state, and other per-session fields. 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, 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 @@ -35,16 +35,42 @@ Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot ### Active session -Once a session reaches `lifecycle: 'ready'`, it accepts turns: +Once a session reaches `lifecycle: 'ready'`, clients may create chats on it with [`createChat`](/reference/chat#createchat). Each chat is independently subscribable at its own `ahp-chat:/` URI; see the [Chat Channel specification](./chat-channel) for the per-chat lifecycle, turn flow, tool calls, and input request handling. -- The client dispatches `session/turnStarted` to begin a turn. -- The server streams `session/delta`, `session/responsePart`, `session/toolCallStart`, `session/toolCallReady`, and related actions. -- The client dispatches `session/toolCallConfirmed` / `session/toolCallResultConfirmed` to approve or deny tool calls, or `session/turnCancelled` to abort. -- The server dispatches `session/turnComplete` or `session/error` when the turn ends. -- The server MAY dispatch `session/inputRequested` while a turn is active. Clients sync answer drafts with `session/inputAnswerChanged` and finish the request with `session/inputCompleted`. +Session-scoped actions dispatched on this channel are limited to: + +- Catalog mutations — `session/chatAdded`, `session/chatRemoved`, `session/chatUpdated`, and `session/defaultChatChanged`. +- Session-wide configuration — model and agent defaults, active-client tracking, customizations, changesets, lifecycle transitions. All actions dispatched on this channel travel on `ActionEnvelope`s whose `channel` is the session URI. Action payloads do NOT carry their own session URI — the channel comes from the envelope. +### Chat catalog mutations + +Three discrete actions keep `SessionState.chats` in sync as chats come and go. Sessions with a single chat trivially round-trip a `session/chatAdded` once at creation; multi-chat sessions exercise all three: + +| Action | Payload | Reducer behavior | +|---|---|---| +| `session/chatAdded` | `summary: ChatSummary` | Upsert by `summary.resource`. Appends when no entry has the same URI; otherwise replaces the existing entry. Mirrors `root/sessionAdded`. | +| `session/chatRemoved` | `chat: URI` | Removes the matching entry. No-op when no entry matches. If `state.defaultChat` referenced the removed URI, the reducer clears it. Mirrors `root/sessionRemoved`. | +| `session/chatUpdated` | `chat: URI, changes: Partial` | Merges the non-identity fields of `changes` onto the matching entry. No-op when no entry matches; clients SHOULD then wait for a `session/chatAdded`. Identity fields (`resource`) MUST NOT be carried in `changes`. Mirrors `root/sessionSummaryChanged`. | + +The producer of the chat's own [`ChatState`](./chat-channel#state) is responsible for emitting matching `session/chatUpdated` actions so the catalog and the per-chat channel stay consistent. + +### Chat aggregation + +[`SessionSummary`](/reference/session#sessionsummary) carries session-wide identity (`resource`, `provider`, `createdAt`, `workingDirectory`) but several of its mutable fields are aggregates derived from the session's chats. Producers SHOULD apply these rules so clients that only consume the session summary (a session list, for example) still see meaningful state: + +| Field | Derivation rule | +|---|---| +| `status` | Take the activity bits (`Idle` / `InProgress` / `InputNeeded` / `Error`) from the [`defaultChat`](#defaultchat) when set, else from the most recently modified chat. Promote `InputNeeded` if **any** chat needs input. Promote `Error` if **any** chat is in an error state. The orthogonal `IsRead` / `IsArchived` flags remain session-scoped and pass through unchanged. | +| `activity` | Mirror the activity string of the chat that contributes the activity bits — usually the default chat, but the chat that raised `InputNeeded` / `Error` when a non-default chat wins the promotion. | +| `modifiedAt` | The maximum of every chat's `modifiedAt`. | +| `model` / `agent` | The session-level selection. Per-chat overrides are surfaced on individual [`ChatSummary`](/reference/chat#chatsummary) entries; do **not** aggregate them up. | +| `workingDirectory` | The session-level **default**. Individual chats MAY override via [`ChatSummary.workingDirectory`](/reference/chat#chatsummary); aggregating per-chat overrides up is meaningless and SHOULD NOT be attempted. | +| `changes` | Optional roll-up. Producers MAY sum per-chat changeset stats or report the most expensive chat's stats — whichever is cheaper to compute. | + +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. + ### Disposal ```jsonc @@ -69,16 +95,14 @@ session URI (`ahp-session:/`). | Method | Kind | Purpose | |---|---|---| | `createSession` | request | Create a session at the chosen URI. | -| `disposeSession` | request | Dispose this session and its backend resources. | -| `fetchTurns` | request | Page historical turns for this session. | -| `completions` | request | Session-scoped inline completions (e.g. user-message mentions). | +| `disposeSession` | request | Dispose this session and its backend resources (cascades to every chat in the session's catalog). | ### Notifications (`params.channel = "ahp-session:/"`) | Method | Kind | Meaning | |---|---|---| -| `action` | server → client notification | Session action envelope (`session/*` action payloads). | -| `dispatchAction` | client → server notification | Dispatch client actions on this session (`session/turnStarted`, `session/toolCallConfirmed`, ...). | +| `action` | server → client notification | Session action envelope (`session/*` action payloads — catalog updates, lifecycle, model/agent, customizations, changesets). | +| `dispatchAction` | client → server notification | Dispatch client actions on this session (`session/modelChanged`, `session/defaultChatChanged`, ...). | | `unsubscribe` | client → server notification | Stop receiving messages for this session channel. | `auth/required` may also target a session URI when auth is required for an @@ -92,37 +116,11 @@ When the server receives a client-dispatched action on this channel, it MUST val | Action | Condition | Server Behavior | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | Any action referencing a non-existent session | Channel URI not found | Server MUST silently ignore the action (no echo) | -| `session/toolCallConfirmed` | Tool call not in `pending-confirmation` state | Server MUST reject the action | -| `session/turnCancelled` | No active turn | Server MUST reject the action | -| `session/modelChanged` | A turn is currently active | Server MUST defer the model change until the active turn completes, then apply it for the next turn | -| `session/agentChanged` | A turn is currently active | Server MUST defer the agent change until the active turn completes, then apply it for the next turn | -| `session/inputAnswerChanged` | No input request with matching `requestId` | Server SHOULD reject the action | -| `session/inputAnswerChanged` | `answer.state` requires a value but `answer.value` is absent, or `answer.value.kind` is missing the matching payload field | Server SHOULD reject the action | -| `session/inputCompleted` | No input request with matching `requestId` | Server SHOULD reject the action | -| `session/inputCompleted` | `response` is `'accept'` but required questions do not have submitted answers | Server SHOULD reject the action | -| `session/pendingMessageRemoved` | No pending message with matching `id` and `kind` | Server SHOULD reject the action | - -## Pending Message Consumption - -The server consumes pending messages according to their kind: - -### Queued Messages - -When a turn completes and `queuedMessages` is non-empty, the server SHOULD: - -1. Dispatch `session/pendingMessageRemoved` with `kind: 'queued'` for the first queued message. -2. Dispatch `session/turnStarted` with the queued message's `message` and `queuedMessageId` set to the message's `id`. - -When a queued message is added while the session is idle (no active turn), the server SHOULD immediately consume it using the same two-step sequence. - -### Steering Messages - -When a turn is active and `steeringMessages` is non-empty, the server MAY consume steering messages at its discretion. To consume a steering message, the server: - -1. Dispatches `session/pendingMessageRemoved` with `kind: 'steering'`. -2. Injects the message content into the model context (the injection mechanism is opaque to the protocol). +| `session/modelChanged` | A turn is currently active in any chat in this session | Server MUST defer the model change until every active turn completes, then apply it for next turns | +| `session/agentChanged` | A turn is currently active in any chat in this session | Server MUST defer the agent change until every active turn completes, then apply it for next turns | +| `session/defaultChatChanged` | `defaultChat` URI does not match an entry in the session's chat catalog | Server MUST reject the action | -Steering messages added while idle are silently stored and consumed when a turn becomes active. +Turn-, tool-call-, input-request-, and pending-message-level validation lives on the [Chat Channel](./chat-channel#server-validation-of-client-actions). ## Actions diff --git a/schema/actions.schema.json b/schema/actions.schema.json index b306559a..a1ff1862 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -128,29 +128,6 @@ "config" ] }, - "ToolCallActionBase": { - "type": "object", - "description": "Base interface for all tool-call-scoped actions, carrying the common turn\nand tool call identifiers. The owning session URI is identified by the\nenclosing {@link ActionEnvelope}'s `channel` field.", - "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - } - }, - "required": [ - "turnId", - "toolCallId" - ] - }, "SessionReadyAction": { "type": "object", "description": "Session backend initialized successfully.", @@ -180,976 +157,1109 @@ "error" ] }, - "SessionTurnStartedAction": { + "SessionChatAddedAction": { "type": "object", - "description": "A new message has been sent to the agent, and a new turn starts.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTurnStarted" - }, - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The new message" + "$ref": "#/$defs/ActionType.SessionChatAdded" }, - "queuedMessageId": { - "type": "string", - "description": "If this turn was auto-started from a queued message, the ID of that message" + "summary": { + "$ref": "#/$defs/ChatSummary", + "description": "The full summary of the newly added (or upserted) chat." } }, "required": [ "type", - "turnId", - "message" + "summary" ] }, - "SessionDeltaAction": { + "SessionChatRemovedAction": { "type": "object", - "description": "Streaming text chunk from the assistant, appended to a specific response part.\n\nThe server MUST first emit a `session/responsePart` to create the target\npart (markdown or reasoning), then use this action to append text to it.", + "description": "A chat was removed from this session's catalog. No-op when no entry matches.\n\nMirrors the root-channel `root/sessionRemoved` notification.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionDelta" - }, - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "partId": { - "type": "string", - "description": "Identifier of the response part to append to" + "$ref": "#/$defs/ActionType.SessionChatRemoved" }, - "content": { - "type": "string", - "description": "Text chunk" + "chat": { + "$ref": "#/$defs/URI", + "description": "The URI of the chat to remove." } }, "required": [ "type", - "turnId", - "partId", - "content" + "chat" ] }, - "SessionResponsePartAction": { + "SessionChatUpdatedAction": { "type": "object", - "description": "Structured content appended to the response.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionResponsePart" + "$ref": "#/$defs/ActionType.SessionChatUpdated" }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "chat": { + "$ref": "#/$defs/URI", + "description": "The URI of the chat whose summary changed." }, - "part": { - "$ref": "#/$defs/ResponsePart", - "description": "Response part (markdown or content ref)" + "changes": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" + }, + "title": { + "type": "string", + "description": "Chat title" + }, + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { + "type": "string", + "description": "Human-readable description of what the chat is currently doing" + }, + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + } + }, + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ "type", - "turnId", - "part" + "chat", + "changes" ] }, - "SessionToolCallStartAction": { + "SessionDefaultChatChangedAction": { "type": "object", - "description": "A tool call begins — parameters are streaming from the LM.\n\nThe server sets {@link ToolCallContributor | `contributor`} to identify\nthe origin of the tool. For client-provided tools, the named client is\nresponsible for executing the tool once it reaches the `running` state\nand dispatching `session/toolCallComplete`. For MCP-served tools, the\nserver executes the call against the named `McpServerCustomization`.", + "description": "The default chat input-routing hint for this session changed.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallStart" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" + "$ref": "#/$defs/ActionType.SessionDefaultChatChanged" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called. Absent for\nserver-side tools that are not contributed by a client or MCP server." + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "New default chat URI, or `undefined` to clear the hint." } }, "required": [ - "turnId", - "toolCallId", - "type", - "toolName", - "displayName" + "type" ] }, - "SessionToolCallDeltaAction": { + "SessionTitleChangedAction": { "type": "object", - "description": "Streaming partial parameters for a tool call.", + "description": "Session title updated. Fired by the server when the title is auto-generated\nfrom conversation, or dispatched by a client to rename a session.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallDelta" + "$ref": "#/$defs/ActionType.SessionTitleChanged" }, - "content": { + "title": { "type": "string", - "description": "Partial parameter content to append" - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Updated progress message" + "description": "New title" } }, "required": [ - "turnId", - "toolCallId", "type", - "content" + "title" ] }, - "SessionToolCallReadyAction": { + "SessionModelChangedAction": { "type": "object", - "description": "Tool call parameters are complete, or a running tool requires re-confirmation.\n\nWhen dispatched for a `streaming` tool call, transitions to `pending-confirmation`\nor directly to `running` if `confirmed` is set.\n\nWhen dispatched for a `running` tool call (e.g. mid-execution permission needed),\ntransitions back to `pending-confirmation`. The `invocationMessage` and `_meta`\nSHOULD be updated to describe the specific confirmation needed. Clients use the\nstandard `session/toolCallConfirmed` flow to approve or deny.\n\nFor client-provided tools, the server typically sets `confirmed` to\n`'not-needed'` so the tool transitions directly to `running`, where the\nowning client can begin execution immediately.", + "description": "Model changed for this session.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallReady" - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do or what confirmation is needed" - }, - "toolInput": { - "type": "string", - "description": "Raw tool input" - }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" - }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "string" - } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" - }, - "editable": { - "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "If set, the tool was auto-confirmed and transitions directly to `running`" + "$ref": "#/$defs/ActionType.SessionModelChanged" }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ConfirmationOption" - }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "New model selection" } }, "required": [ - "turnId", - "toolCallId", "type", - "invocationMessage" + "model" ] }, - "SessionToolCallApprovedAction": { + "SessionAgentChangedAction": { "type": "object", - "description": "Client approves a pending tool call. The tool transitions to `running`.", + "description": "Custom agent selection changed for this session.\n\nOmitting `agent` (or setting it to `undefined`) clears the selection and\nresets the session to no selected custom agent (provider default behavior).\n\nWhen a turn is currently active, the server MUST defer the change until\nthe active turn completes, then apply it for the next turn (same rule as\n{@link SessionModelChangedAction | `session/modelChanged`}).", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallConfirmed" - }, - "approved": { - "description": "The tool call was approved" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed" - }, - "editedToolInput": { - "type": "string", - "description": "Edited tool input parameters, if the client modified them before confirming" + "$ref": "#/$defs/ActionType.SessionAgentChanged" }, - "selectedOptionId": { - "type": "string", - "description": "ID of the selected confirmation option, if the server provided options" + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "New agent selection, or `undefined` to clear the selection and reset the\nsession to no selected custom agent." } }, "required": [ - "turnId", - "toolCallId", - "type", - "approved", - "confirmed" + "type" ] }, - "SessionToolCallDeniedAction": { + "SessionIsReadChangedAction": { "type": "object", - "description": "Client denies a pending tool call. The tool transitions to `cancelled`.\n\nFor client-provided tools, the owning client MUST dispatch this if it does\nnot recognize the tool or cannot execute it.", + "description": "The read state of the session changed.\n\nDispatched by a client to mark a session as read (e.g. after viewing it)\nor unread (e.g. after new activity since the client last looked at it).", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallConfirmed" + "$ref": "#/$defs/ActionType.SessionIsReadChanged" }, - "approved": { - "description": "The tool call was denied" - }, - "reason": { - "oneOf": [ - { - "$ref": "#/$defs/ToolCallCancellationReason.Denied" - }, - { - "$ref": "#/$defs/ToolCallCancellationReason.Skipped" - } - ], - "description": "Why the tool was cancelled" - }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" - }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional explanation for the denial" - }, - "selectedOptionId": { - "type": "string", - "description": "ID of the selected confirmation option, if the server provided options" + "isRead": { + "type": "boolean", + "description": "Whether the session has been read" } }, "required": [ - "turnId", - "toolCallId", "type", - "approved", - "reason" + "isRead" ] }, - "SessionToolCallCompleteAction": { + "SessionIsArchivedChangedAction": { "type": "object", - "description": "Tool execution finished. Transitions to `completed` or `pending-result-confirmation`\nif `requiresResultConfirmation` is `true`.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action with the execution result. The server\nSHOULD reject this action if the dispatching client does not match `toolClientId`.\n\nServers waiting on a client tool call MAY time out after a reasonable duration\nif the implementing client disconnects or becomes unresponsive, and dispatch\nthis action with `result.success = false` and an appropriate error.", + "description": "The archived state of the session changed.\n\nDispatched by a client to archive a session (e.g. the task is\ncomplete) or to unarchive it.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallComplete" - }, - "result": { - "$ref": "#/$defs/ToolCallResult", - "description": "Execution result" + "$ref": "#/$defs/ActionType.SessionIsArchivedChanged" }, - "requiresResultConfirmation": { + "isArchived": { "type": "boolean", - "description": "If true, the result requires client approval before finalizing" + "description": "Whether the session is archived" } }, "required": [ - "turnId", - "toolCallId", "type", - "result" + "isArchived" ] }, - "SessionToolCallResultConfirmedAction": { + "SessionActivityChangedAction": { "type": "object", - "description": "Client approves or denies a tool's result.\n\nIf `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`.", + "description": "The activity description of the session changed.\n\nDispatched by the server to indicate what the session is currently doing\n(e.g. running a tool, thinking). Clear activity by setting it to `undefined`.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallResultConfirmed" + "$ref": "#/$defs/ActionType.SessionActivityChanged" }, - "approved": { - "type": "boolean", - "description": "Whether the result was approved" + "activity": { + "type": "string", + "description": "Human-readable description of current activity, or `undefined` to clear" } }, "required": [ - "turnId", - "toolCallId", "type", - "approved" + "activity" ] }, - "SessionToolCallContentChangedAction": { + "SessionChangesetsChangedAction": { "type": "object", - "description": "Partial content produced while a tool is still executing.\n\nReplaces the `content` array on the running tool call state. Clients can\nuse this to display live feedback (e.g. a terminal reference) before the\ntool completes.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action to stream intermediate content while\nexecuting. The server SHOULD reject this action if the dispatching client does\nnot match `toolClientId`.", + "description": "The {@link Changeset | catalogue of changesets} the agent host\nadvertises for this session changed. Replaces\n{@link SessionState.changesets | `state.changesets`} entirely\n(full-replacement semantics) — set to `undefined` to clear the\ncatalogue.\n\nProducers dispatch this whenever entries are added or removed. The\nfan-out happens through this action so observers see catalogue\nmutations in the same {@link ChangesetAction | per-changeset} action\nstream they already follow for file-level updates.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallContentChanged" + "$ref": "#/$defs/ActionType.SessionChangesetsChanged" }, - "content": { + "changesets": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/Changeset" }, - "description": "The current partial content for the running tool call" + "description": "New catalogue, or `undefined` to clear it" } }, "required": [ - "turnId", - "toolCallId", "type", - "content" + "changesets" ] }, - "SessionTurnCompleteAction": { + "SessionServerToolsChangedAction": { "type": "object", - "description": "Turn finished — the assistant is idle.", + "description": "Server tools for this session have changed.\n\nFull-replacement semantics: the `tools` array replaces the previous `serverTools` entirely.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTurnComplete" + "$ref": "#/$defs/ActionType.SessionServerToolsChanged" }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "tools": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolDefinition" + }, + "description": "Updated server tools list (full replacement)" } }, "required": [ "type", - "turnId" + "tools" ] }, - "SessionTurnCancelledAction": { + "SessionActiveClientChangedAction": { "type": "object", - "description": "Turn was aborted; server stops processing.", + "description": "The active client for this session has changed.\n\nA client dispatches this action with its own `SessionActiveClient` to claim\nthe active role, or with `null` to release it. The server SHOULD reject if\nanother client is already active. The server SHOULD automatically dispatch\nthis action with `activeClient: null` when the active client disconnects.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTurnCancelled" + "$ref": "#/$defs/ActionType.SessionActiveClientChanged" }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "activeClient": { + "oneOf": [ + { + "$ref": "#/$defs/SessionActiveClient" + }, + {} + ], + "description": "The new active client, or `null` to unset" } }, "required": [ "type", - "turnId" + "activeClient" ] }, - "SessionErrorAction": { + "SessionActiveClientToolsChangedAction": { "type": "object", - "description": "Error during turn processing.", + "description": "The active client's tool list has changed.\n\nFull-replacement semantics: the `tools` array replaces the active client's\nprevious tools entirely. The server SHOULD reject if the dispatching client\nis not the current active client.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionError" - }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "$ref": "#/$defs/ActionType.SessionActiveClientToolsChanged" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details" + "tools": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolDefinition" + }, + "description": "Updated client tools list (full replacement)" } }, "required": [ "type", - "turnId", - "error" + "tools" ] }, - "SessionTitleChangedAction": { + "SessionCustomizationsChangedAction": { "type": "object", - "description": "Session title updated. Fired by the server when the title is auto-generated\nfrom conversation, or dispatched by a client to rename a session.", + "description": "The session's customizations have changed.\n\nFull-replacement semantics: the `customizations` array replaces the\nprevious `customizations` entirely.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTitleChanged" + "$ref": "#/$defs/ActionType.SessionCustomizationsChanged" }, - "title": { - "type": "string", - "description": "New title" + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/Customization" + }, + "description": "Updated customization list (full replacement)." } }, "required": [ "type", - "title" + "customizations" ] }, - "SessionUsageAction": { + "SessionCustomizationToggledAction": { "type": "object", - "description": "Token usage report for a turn.", + "description": "A client toggled a container customization on or off.\n\nTargets a top-level container (plugin or directory) by `id`. Only\ncontainers have an `enabled` flag; children are always active when\ntheir container is enabled. Is a no-op when no matching container is\nfound.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionUsage" + "$ref": "#/$defs/ActionType.SessionCustomizationToggled" }, - "turnId": { + "id": { "type": "string", - "description": "Turn identifier" + "description": "The id of the container to toggle." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage data" + "enabled": { + "type": "boolean", + "description": "Whether to enable or disable the container." } }, "required": [ "type", - "turnId", - "usage" + "id", + "enabled" ] }, - "SessionReasoningAction": { + "SessionCustomizationUpdatedAction": { "type": "object", - "description": "Reasoning/thinking text from the model, appended to a specific reasoning response part.\n\nThe server MUST first emit a `session/responsePart` to create the target\nreasoning part, then use this action to append text to it.", + "description": "Upserts a top-level customization (plugin or directory).\n\nThe reducer locates the existing entry by `customization.id`:\n\n- If found, the entry is replaced entirely with `customization`,\n including its `children` array. To preserve existing children, the\n host must include them on the payload.\n- If not found, the entry is appended.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionReasoning" - }, - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "partId": { - "type": "string", - "description": "Identifier of the reasoning response part to append to" + "$ref": "#/$defs/ActionType.SessionCustomizationUpdated" }, - "content": { - "type": "string", - "description": "Reasoning text chunk" + "customization": { + "$ref": "#/$defs/Customization", + "description": "The customization to upsert (matched by `customization.id`)." } }, "required": [ "type", - "turnId", - "partId", - "content" + "customization" ] }, - "SessionModelChangedAction": { + "SessionCustomizationRemovedAction": { "type": "object", - "description": "Model changed for this session.", + "description": "Removes a customization by id.\n\nSearches every container and its children for the entry. If the entry\nis a container, its children are removed with it. Is a no-op when no\nmatching id is found.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionModelChanged" + "$ref": "#/$defs/ActionType.SessionCustomizationRemoved" }, - "model": { - "$ref": "#/$defs/ModelSelection", - "description": "New model selection" + "id": { + "type": "string", + "description": "The id of the customization to remove." } }, "required": [ "type", - "model" + "id" ] }, - "SessionAgentChangedAction": { + "SessionMcpServerStateChangedAction": { "type": "object", - "description": "Custom agent selection changed for this session.\n\nOmitting `agent` (or setting it to `undefined`) clears the selection and\nresets the session to no selected custom agent (provider default behavior).\n\nWhen a turn is currently active, the server MUST defer the change until\nthe active turn completes, then apply it for the next turn (same rule as\n{@link SessionModelChangedAction | `session/modelChanged`}).", + "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.state | `state`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatus.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerAuthRequiredState} for the rationale.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionAgentChanged" + "$ref": "#/$defs/ActionType.SessionMcpServerStateChanged" }, - "agent": { - "$ref": "#/$defs/AgentSelection", - "description": "New agent selection, or `undefined` to clear the selection and reset the\nsession to no selected custom agent." + "id": { + "type": "string", + "description": "The id of the {@link McpServerCustomization} to update." + }, + "state": { + "$ref": "#/$defs/McpServerState", + "description": "The new lifecycle state." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatus.Ready | `Ready`})." } }, "required": [ - "type" + "type", + "id", + "state" ] }, - "SessionIsReadChangedAction": { + "SessionConfigChangedAction": { "type": "object", - "description": "The read state of the session changed.\n\nDispatched by a client to mark a session as read (e.g. after viewing it)\nor unread (e.g. after new activity since the client last looked at it).", + "description": "Client changed a mutable config value mid-session.\n\nOnly properties with `sessionMutable: true` in the config schema may be\nchanged. The server validates and broadcasts the action; the reducer merges\nthe new values into `state.config.values`.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionIsReadChanged" + "$ref": "#/$defs/ActionType.SessionConfigChanged" }, - "isRead": { + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Updated config values" + }, + "replace": { "type": "boolean", - "description": "Whether the session has been read" + "description": "When `true`, replaces all config values instead of merging" } }, "required": [ "type", - "isRead" + "config" ] }, - "SessionIsArchivedChangedAction": { + "SessionMetaChangedAction": { "type": "object", - "description": "The archived state of the session changed.\n\nDispatched by a client to archive a session (e.g. the task is\ncomplete) or to unarchive it.", + "description": "The session's `_meta` side-channel changed. Replaces `state._meta`\nentirely (full-replacement semantics). Producers SHOULD merge any\nkeys they wish to preserve into the new value before dispatching.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionIsArchivedChanged" + "$ref": "#/$defs/ActionType.SessionMetaChanged" }, - "isArchived": { - "type": "boolean", - "description": "Whether the session is archived" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "New `_meta` payload, or `undefined` to clear it" } }, "required": [ "type", - "isArchived" + "_meta" ] }, - "SessionActivityChangedAction": { + "ToolCallActionBase": { "type": "object", - "description": "The activity description of the session changed.\n\nDispatched by the server to indicate what the session is currently doing\n(e.g. running a tool, thinking). Clear activity by setting it to `undefined`.", + "description": "Base interface for all tool-call-scoped actions, carrying the common turn\nand tool call identifiers. The owning chat URI is identified by the\nenclosing {@link ActionEnvelope}'s `channel` field.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionActivityChanged" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "activity": { + "toolCallId": { "type": "string", - "description": "Human-readable description of current activity, or `undefined` to clear" - } - }, - "required": [ - "type", - "activity" - ] - }, - "SessionChangesetsChangedAction": { - "type": "object", - "description": "The {@link Changeset | catalogue of changesets} the agent host\nadvertises for this session changed. Replaces\n{@link SessionState.changesets | `state.changesets`} entirely\n(full-replacement semantics) — set to `undefined` to clear the\ncatalogue.\n\nProducers dispatch this whenever entries are added or removed. The\nfan-out happens through this action so observers see catalogue\nmutations in the same {@link ChangesetAction | per-changeset} action\nstream they already follow for file-level updates.", - "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionChangesetsChanged" + "description": "Tool call identifier" }, - "changesets": { - "type": "array", - "items": { - "$ref": "#/$defs/Changeset" - }, - "description": "New catalogue, or `undefined` to clear it" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." } }, "required": [ - "type", - "changesets" + "turnId", + "toolCallId" ] }, - "SessionServerToolsChangedAction": { + "ChatTurnStartedAction": { "type": "object", - "description": "Server tools for this session have changed.\n\nFull-replacement semantics: the `tools` array replaces the previous `serverTools` entirely.", + "description": "A new message has been sent to the agent, and a new turn starts.\n\nA client is only allowed to send {@link MessageKind.User} messages.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionServerToolsChanged" + "$ref": "#/$defs/ActionType.ChatTurnStarted" }, - "tools": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolDefinition" - }, - "description": "Updated server tools list (full replacement)" - } - }, - "required": [ - "type", - "tools" - ] - }, - "SessionActiveClientChangedAction": { - "type": "object", - "description": "The active client for this session has changed.\n\nA client dispatches this action with its own `SessionActiveClient` to claim\nthe active role, or with `null` to release it. The server SHOULD reject if\nanother client is already active. The server SHOULD automatically dispatch\nthis action with `activeClient: null` when the active client disconnects.", - "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionActiveClientChanged" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "activeClient": { - "oneOf": [ - { - "$ref": "#/$defs/SessionActiveClient" - }, - {} - ], - "description": "The new active client, or `null` to unset" - } - }, - "required": [ - "type", - "activeClient" - ] - }, - "SessionActiveClientToolsChangedAction": { - "type": "object", - "description": "The active client's tool list has changed.\n\nFull-replacement semantics: the `tools` array replaces the active client's\nprevious tools entirely. The server SHOULD reject if the dispatching client\nis not the current active client.", - "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionActiveClientToolsChanged" + "message": { + "$ref": "#/$defs/Message", + "description": "The new message" }, - "tools": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolDefinition" - }, - "description": "Updated client tools list (full replacement)" + "queuedMessageId": { + "type": "string", + "description": "If this turn was auto-started from a queued message, the ID of that message" } }, "required": [ "type", - "tools" + "turnId", + "message" ] }, - "SessionCustomizationsChangedAction": { + "ChatDeltaAction": { "type": "object", - "description": "The session's customizations have changed.\n\nFull-replacement semantics: the `customizations` array replaces the\nprevious `customizations` entirely.", + "description": "Streaming text chunk from the assistant, appended to a specific response part.\n\nThe server MUST first emit a `chat/responsePart` to create the target\npart (markdown or reasoning), then use this action to append text to it.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationsChanged" + "$ref": "#/$defs/ActionType.ChatDelta" }, - "customizations": { - "type": "array", - "items": { - "$ref": "#/$defs/Customization" - }, - "description": "Updated customization list (full replacement)." + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "partId": { + "type": "string", + "description": "Identifier of the response part to append to" + }, + "content": { + "type": "string", + "description": "Text chunk" } }, "required": [ "type", - "customizations" + "turnId", + "partId", + "content" ] }, - "SessionCustomizationToggledAction": { + "ChatResponsePartAction": { "type": "object", - "description": "A client toggled a container customization on or off.\n\nTargets a top-level container (plugin or directory) by `id`. Only\ncontainers have an `enabled` flag; children are always active when\ntheir container is enabled. Is a no-op when no matching container is\nfound.", + "description": "Structured content appended to the response.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationToggled" + "$ref": "#/$defs/ActionType.ChatResponsePart" }, - "id": { + "turnId": { "type": "string", - "description": "The id of the container to toggle." + "description": "Turn identifier" }, - "enabled": { - "type": "boolean", - "description": "Whether to enable or disable the container." + "part": { + "$ref": "#/$defs/ResponsePart", + "description": "Response part (markdown or content ref)" } }, "required": [ "type", - "id", - "enabled" + "turnId", + "part" ] }, - "SessionCustomizationUpdatedAction": { + "ChatToolCallStartAction": { "type": "object", - "description": "Upserts a top-level customization (plugin or directory).\n\nThe reducer locates the existing entry by `customization.id`:\n\n- If found, the entry is replaced entirely with `customization`,\n including its `children` array. To preserve existing children, the\n host must include them on the payload.\n- If not found, the entry is appended.", + "description": "A tool call begins — parameters are streaming from the LM.\n\nThe server sets {@link ToolCallContributor | `contributor`} to identify\nthe origin of the tool. For client-provided tools, the named client is\nresponsible for executing the tool once it reaches the `running` state\nand dispatching `chat/toolCallComplete`. For MCP-served tools, the\nserver executes the call against the named `McpServerCustomization`.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationUpdated" + "$ref": "#/$defs/ActionType.ChatToolCallStart" }, - "customization": { - "$ref": "#/$defs/Customization", - "description": "The customization to upsert (matched by `customization.id`)." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called. Absent for\nserver-side tools that are not contributed by a client or MCP server." } }, "required": [ + "turnId", + "toolCallId", "type", - "customization" + "toolName", + "displayName" ] }, - "SessionCustomizationRemovedAction": { + "ChatToolCallDeltaAction": { "type": "object", - "description": "Removes a customization by id.\n\nSearches every container and its children for the entry. If the entry\nis a container, its children are removed with it. Is a no-op when no\nmatching id is found.", + "description": "Streaming partial parameters for a tool call.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationRemoved" + "$ref": "#/$defs/ActionType.ChatToolCallDelta" }, - "id": { + "content": { "type": "string", - "description": "The id of the customization to remove." + "description": "Partial parameter content to append" + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Updated progress message" } }, "required": [ + "turnId", + "toolCallId", "type", - "id" + "content" ] }, - "SessionMcpServerStateChangedAction": { + "ChatToolCallReadyAction": { "type": "object", - "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.state | `state`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatus.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerAuthRequiredState} for the rationale.", + "description": "Tool call parameters are complete, or a running tool requires re-confirmation.\n\nWhen dispatched for a `streaming` tool call, transitions to `pending-confirmation`\nor directly to `running` if `confirmed` is set.\n\nWhen dispatched for a `running` tool call (e.g. mid-execution permission needed),\ntransitions back to `pending-confirmation`. The `invocationMessage` and `_meta`\nSHOULD be updated to describe the specific confirmation needed. Clients use the\nstandard `chat/toolCallConfirmed` flow to approve or deny.\n\nFor client-provided tools, the server typically sets `confirmed` to\n`'not-needed'` so the tool transitions directly to `running`, where the\nowning client can begin execution immediately.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionMcpServerStateChanged" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "id": { + "toolCallId": { "type": "string", - "description": "The id of the {@link McpServerCustomization} to update." + "description": "Tool call identifier" }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "The new lifecycle state." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." }, - "channel": { - "$ref": "#/$defs/URI", - "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatus.Ready | `Ready`})." - } - }, - "required": [ - "type", - "id", - "state" - ] - }, - "SessionConfigChangedAction": { - "type": "object", - "description": "Client changed a mutable config value mid-session.\n\nOnly properties with `sessionMutable: true` in the config schema may be\nchanged. The server validates and broadcasts the action; the reducer merges\nthe new values into `state.config.values`.", - "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionConfigChanged" + "$ref": "#/$defs/ActionType.ChatToolCallReady" }, - "config": { + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do or what confirmation is needed" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "edits": { "type": "object", - "additionalProperties": {}, - "description": "Updated config values" + "properties": { + "items": { + "type": "string" + } + }, + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" }, - "replace": { + "editable": { "type": "boolean", - "description": "When `true`, replaces all config values instead of merging" + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "If set, the tool was auto-confirmed and transitions directly to `running`" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfirmationOption" + }, + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." } }, "required": [ + "turnId", + "toolCallId", "type", - "config" + "invocationMessage" ] }, - "SessionMetaChangedAction": { + "ChatToolCallApprovedAction": { "type": "object", - "description": "The session's `_meta` side-channel changed. Replaces `state._meta`\nentirely (full-replacement semantics). Producers SHOULD merge any\nkeys they wish to preserve into the new value before dispatching.", + "description": "Client approves a pending tool call. The tool transitions to `running`.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionMetaChanged" + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "New `_meta` payload, or `undefined` to clear it" + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, + "type": { + "$ref": "#/$defs/ActionType.ChatToolCallConfirmed" + }, + "approved": { + "description": "The tool call was approved" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed" + }, + "editedToolInput": { + "type": "string", + "description": "Edited tool input parameters, if the client modified them before confirming" + }, + "selectedOptionId": { + "type": "string", + "description": "ID of the selected confirmation option, if the server provided options" } }, "required": [ + "turnId", + "toolCallId", "type", - "_meta" + "approved", + "confirmed" ] }, - "SessionTruncatedAction": { + "ChatToolCallDeniedAction": { "type": "object", - "description": "Truncates a session's history. If `turnId` is provided, all turns after that\nturn are removed and the specified turn is kept. If `turnId` is omitted, all\nturns are removed.\n\nIf there is an active turn it is silently dropped and the session status\nreturns to `idle`.\n\nCommon use-case: truncate old data then dispatch a new\n`session/turnStarted` with an edited message.", + "description": "Client denies a pending tool call. The tool transitions to `cancelled`.\n\nFor client-provided tools, the owning client MUST dispatch this if it does\nnot recognize the tool or cannot execute it.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionTruncated" + "$ref": "#/$defs/ActionType.ChatToolCallConfirmed" }, - "turnId": { + "approved": { + "description": "The tool call was denied" + }, + "reason": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallCancellationReason.Denied" + }, + { + "$ref": "#/$defs/ToolCallCancellationReason.Skipped" + } + ], + "description": "Why the tool was cancelled" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional explanation for the denial" + }, + "selectedOptionId": { "type": "string", - "description": "Keep turns up to and including this turn. Omit to clear all turns." + "description": "ID of the selected confirmation option, if the server provided options" } }, "required": [ - "type" + "turnId", + "toolCallId", + "type", + "approved", + "reason" ] }, - "SessionPendingMessageSetAction": { + "ChatToolCallCompleteAction": { "type": "object", - "description": "A pending message was set (upsert semantics: creates or replaces).\n\nFor steering messages, this always replaces the single steering message.\nFor queued messages, if a message with the given `id` already exists it is\nupdated in place; otherwise it is appended to the queue. If the session is\nidle when a queued message is set, the server SHOULD immediately consume it\nand start a new turn.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "Tool execution finished. Transitions to `completed` or `pending-result-confirmation`\nif `requiresResultConfirmation` is `true`.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action with the execution result. The server\nSHOULD reject this action if the dispatching client does not match `toolClientId`.\n\nServers waiting on a client tool call MAY time out after a reasonable duration\nif the implementing client disconnects or becomes unresponsive, and dispatch\nthis action with `result.success = false` and an appropriate error.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionPendingMessageSet" - }, - "kind": { - "$ref": "#/$defs/PendingMessageKind", - "description": "Whether this is a steering or queued message" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "id": { + "toolCallId": { "type": "string", - "description": "Unique identifier for this pending message" + "description": "Tool call identifier" }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message content" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, + "type": { + "$ref": "#/$defs/ActionType.ChatToolCallComplete" + }, + "result": { + "$ref": "#/$defs/ToolCallResult", + "description": "Execution result" + }, + "requiresResultConfirmation": { + "type": "boolean", + "description": "If true, the result requires client approval before finalizing" } }, "required": [ + "turnId", + "toolCallId", "type", - "kind", - "id", - "message" + "result" ] }, - "SessionPendingMessageRemovedAction": { + "ChatToolCallResultConfirmedAction": { "type": "object", - "description": "A pending message was removed (steering or queued).\n\nDispatched by clients to cancel a pending message, or by the server when\nit consumes a message (e.g. starting a turn from a queued message or\ninjecting a steering message into the current turn).", + "description": "Client approves or denies a tool's result.\n\nIf `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionPendingMessageRemoved" - }, - "kind": { - "$ref": "#/$defs/PendingMessageKind", - "description": "Whether this is a steering or queued message" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "id": { + "toolCallId": { "type": "string", - "description": "Identifier of the pending message to remove" + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, + "type": { + "$ref": "#/$defs/ActionType.ChatToolCallResultConfirmed" + }, + "approved": { + "type": "boolean", + "description": "Whether the result was approved" } }, "required": [ + "turnId", + "toolCallId", "type", - "kind", - "id" + "approved" ] }, - "SessionQueuedMessagesReorderedAction": { + "ChatToolCallContentChangedAction": { "type": "object", - "description": "Reorder the queued messages.\n\nThe `order` array contains the IDs of queued messages in their new\ndesired order. IDs not present in the current queue are ignored.\nQueued messages whose IDs are absent from `order` are appended at\nthe end in their original relative order (so a client with a stale\nview of the queue never silently drops messages).", + "description": "Partial content produced while a tool is still executing.\n\nReplaces the `content` array on the running tool call state. Clients can\nuse this to display live feedback (e.g. a terminal reference) before the\ntool completes.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action to stream intermediate content while\nexecuting. The server SHOULD reject this action if the dispatching client does\nnot match `toolClientId`.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionQueuedMessagesReordered" + "$ref": "#/$defs/ActionType.ChatToolCallContentChanged" }, - "order": { + "content": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Queued message IDs in the desired order" + "description": "The current partial content for the running tool call" } }, "required": [ + "turnId", + "toolCallId", "type", - "order" + "content" ] }, - "SessionInputRequestedAction": { + "ChatTurnCompleteAction": { "type": "object", - "description": "A session requested input from the user.\n\nFull-request upsert semantics: the `request` replaces any existing request\nwith the same `id`, or is appended if it is new. Answer drafts are preserved\nunless `request.answers` is provided.", + "description": "Turn finished — the assistant is idle.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionInputRequested" + "$ref": "#/$defs/ActionType.ChatTurnComplete" }, - "request": { - "$ref": "#/$defs/SessionInputRequest", - "description": "Input request to create or replace" + "turnId": { + "type": "string", + "description": "Turn identifier" } }, "required": [ "type", - "request" + "turnId" ] }, - "SessionInputAnswerChangedAction": { + "ChatTurnCancelledAction": { "type": "object", - "description": "A client updated, submitted, skipped, or removed a single in-progress answer.\n\nDispatching with `answer: undefined` removes that question's answer draft.", + "description": "Turn was aborted; server stops processing.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionInputAnswerChanged" - }, - "requestId": { - "type": "string", - "description": "Input request identifier" + "$ref": "#/$defs/ActionType.ChatTurnCancelled" }, - "questionId": { + "turnId": { "type": "string", - "description": "Question identifier within the input request" + "description": "Turn identifier" + } + }, + "required": [ + "type", + "turnId" + ] + }, + "ChatErrorAction": { + "type": "object", + "description": "Error during turn processing.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatError" + }, + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details" + } + }, + "required": [ + "type", + "turnId", + "error" + ] + }, + "ChatUsageAction": { + "type": "object", + "description": "Token usage report for a turn.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatUsage" + }, + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage data" + } + }, + "required": [ + "type", + "turnId", + "usage" + ] + }, + "ChatReasoningAction": { + "type": "object", + "description": "Reasoning/thinking text from the model, appended to a specific reasoning response part.\n\nThe server MUST first emit a `chat/responsePart` to create the target\nreasoning part, then use this action to append text to it.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatReasoning" + }, + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "partId": { + "type": "string", + "description": "Identifier of the reasoning response part to append to" + }, + "content": { + "type": "string", + "description": "Reasoning text chunk" + } + }, + "required": [ + "type", + "turnId", + "partId", + "content" + ] + }, + "ChatTruncatedAction": { + "type": "object", + "description": "Truncates a session's history. If `turnId` is provided, all turns after that\nturn are removed and the specified turn is kept. If `turnId` is omitted, all\nturns are removed.\n\nIf there is an active turn it is silently dropped and the chat status\nreturns to `idle`.\n\nCommon use-case: truncate old data then dispatch a new\n`chat/turnStarted` with an edited message.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatTruncated" + }, + "turnId": { + "type": "string", + "description": "Keep turns up to and including this turn. Omit to clear all turns." + } + }, + "required": [ + "type" + ] + }, + "ChatPendingMessageSetAction": { + "type": "object", + "description": "A pending message was set (upsert semantics: creates or replaces).\n\nFor steering messages, this always replaces the single steering message.\nFor queued messages, if a message with the given `id` already exists it is\nupdated in place; otherwise it is appended to the queue. If the chat is\nidle when a queued message is set, the server SHOULD immediately consume it\nand start a new turn.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatPendingMessageSet" + }, + "kind": { + "$ref": "#/$defs/PendingMessageKind", + "description": "Whether this is a steering or queued message" + }, + "id": { + "type": "string", + "description": "Unique identifier for this pending message" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "The message content" + } + }, + "required": [ + "type", + "kind", + "id", + "message" + ] + }, + "ChatPendingMessageRemovedAction": { + "type": "object", + "description": "A pending message was removed (steering or queued).\n\nDispatched by clients to cancel a pending message, or by the server when\nit consumes a message (e.g. starting a turn from a queued message or\ninjecting a steering message into the current turn).", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatPendingMessageRemoved" + }, + "kind": { + "$ref": "#/$defs/PendingMessageKind", + "description": "Whether this is a steering or queued message" + }, + "id": { + "type": "string", + "description": "Identifier of the pending message to remove" + } + }, + "required": [ + "type", + "kind", + "id" + ] + }, + "ChatQueuedMessagesReorderedAction": { + "type": "object", + "description": "Reorder the queued messages.\n\nThe `order` array contains the IDs of queued messages in their new\ndesired order. IDs not present in the current queue are ignored.\nQueued messages whose IDs are absent from `order` are appended at\nthe end in their original relative order (so a client with a stale\nview of the queue never silently drops messages).", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatQueuedMessagesReordered" + }, + "order": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Queued message IDs in the desired order" + } + }, + "required": [ + "type", + "order" + ] + }, + "ChatInputRequestedAction": { + "type": "object", + "description": "A session requested input from the user.\n\nFull-request upsert semantics: the `request` replaces any existing request\nwith the same `id`, or is appended if it is new. Answer drafts are preserved\nunless `request.answers` is provided.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatInputRequested" + }, + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "Input request to create or replace" + } + }, + "required": [ + "type", + "request" + ] + }, + "ChatInputAnswerChangedAction": { + "type": "object", + "description": "A client updated, submitted, skipped, or removed a single in-progress answer.\n\nDispatching with `answer: undefined` removes that question's answer draft.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.ChatInputAnswerChanged" + }, + "requestId": { + "type": "string", + "description": "Input request identifier" + }, + "questionId": { + "type": "string", + "description": "Question identifier within the input request" }, "answer": { - "$ref": "#/$defs/SessionInputAnswer", + "$ref": "#/$defs/ChatInputAnswer", "description": "Updated answer, or `undefined` to clear an answer draft" } }, @@ -1159,25 +1269,25 @@ "questionId" ] }, - "SessionInputCompletedAction": { + "ChatInputCompletedAction": { "type": "object", "description": "A client accepted, declined, or cancelled a session input request.\n\nIf accepted, the server uses `answers` (when provided) plus the request's\nsynced answer state to resume the blocked operation.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionInputCompleted" + "$ref": "#/$defs/ActionType.ChatInputCompleted" }, "requestId": { "type": "string", "description": "Input request identifier" }, "response": { - "$ref": "#/$defs/SessionInputResponseKind", + "$ref": "#/$defs/ChatInputResponseKind", "description": "Completion outcome" }, "answers": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/SessionInputAnswer" + "$ref": "#/$defs/ChatInputAnswer" }, "description": "Optional final answer replacement, keyed by question ID" } @@ -1636,18 +1746,89 @@ "changes" ] }, - "SessionToolCallConfirmedAction": { + "ChatToolCallConfirmedAction": { "oneOf": [ {}, { - "$ref": "#/$defs/SessionToolCallApprovedAction" + "$ref": "#/$defs/ChatToolCallApprovedAction" }, { - "$ref": "#/$defs/SessionToolCallDeniedAction" + "$ref": "#/$defs/ChatToolCallDeniedAction" } ], "description": "Client confirms or denies a pending tool call." }, + "ChatAction": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/ChatTurnStartedAction" + }, + { + "$ref": "#/$defs/ChatDeltaAction" + }, + { + "$ref": "#/$defs/ChatResponsePartAction" + }, + { + "$ref": "#/$defs/ChatToolCallStartAction" + }, + { + "$ref": "#/$defs/ChatToolCallDeltaAction" + }, + { + "$ref": "#/$defs/ChatToolCallReadyAction" + }, + { + "$ref": "#/$defs/ChatToolCallConfirmedAction" + }, + { + "$ref": "#/$defs/ChatToolCallCompleteAction" + }, + { + "$ref": "#/$defs/ChatToolCallResultConfirmedAction" + }, + { + "$ref": "#/$defs/ChatToolCallContentChangedAction" + }, + { + "$ref": "#/$defs/ChatTurnCompleteAction" + }, + { + "$ref": "#/$defs/ChatTurnCancelledAction" + }, + { + "$ref": "#/$defs/ChatErrorAction" + }, + { + "$ref": "#/$defs/ChatUsageAction" + }, + { + "$ref": "#/$defs/ChatReasoningAction" + }, + { + "$ref": "#/$defs/ChatTruncatedAction" + }, + { + "$ref": "#/$defs/ChatPendingMessageSetAction" + }, + { + "$ref": "#/$defs/ChatPendingMessageRemovedAction" + }, + { + "$ref": "#/$defs/ChatQueuedMessagesReorderedAction" + }, + { + "$ref": "#/$defs/ChatInputRequestedAction" + }, + { + "$ref": "#/$defs/ChatInputAnswerChangedAction" + }, + { + "$ref": "#/$defs/ChatInputCompletedAction" + } + ] + }, "Icon": { "type": "object", "description": "An optionally-sized icon that can be displayed in a user interface.", @@ -2038,7 +2219,7 @@ "properties": { "resource": { "$ref": "#/$defs/URI", - "description": "The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`)" + "description": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" }, "state": { "oneOf": [ @@ -2236,24 +2417,6 @@ "values" ] }, - "PendingMessage": { - "type": "object", - "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this pending message" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that will start the next turn" - } - }, - "required": [ - "id", - "message" - ] - }, "SessionState": { "type": "object", "description": "Full state for a single session, loaded when a client subscribes to the session's URI.", @@ -2281,43 +2444,25 @@ "$ref": "#/$defs/SessionActiveClient", "description": "The client currently providing tools and interactive capabilities to this session" }, - "turns": { + "chats": { "type": "array", "items": { - "$ref": "#/$defs/Turn" + "$ref": "#/$defs/ChatSummary" }, - "description": "Completed turns" + "description": "Catalog of chats in this session." }, - "activeTurn": { - "$ref": "#/$defs/ActiveTurn", - "description": "Currently in-progress turn" + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "The chat that receives input when the user addresses the session without\nselecting a specific chat. This is a UI routing hint, not a hierarchy\nmarker — chats remain equal peers at the protocol level. Hosts MAY change\nthis over the session's lifetime." }, - "steeringMessage": { - "$ref": "#/$defs/PendingMessage", - "description": "Message to inject into the current turn at a convenient point" + "config": { + "$ref": "#/$defs/SessionConfigState", + "description": "Session configuration schema and current values" }, - "queuedMessages": { + "customizations": { "type": "array", "items": { - "$ref": "#/$defs/PendingMessage" - }, - "description": "Messages to send automatically as new turns after the current turn finishes" - }, - "inputRequests": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputRequest" - }, - "description": "Requests for user input that are currently blocking or informing session progress" - }, - "config": { - "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values" - }, - "customizations": { - "type": "array", - "items": { - "$ref": "#/$defs/Customization" + "$ref": "#/$defs/Customization" }, "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClient.customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." }, @@ -2337,7 +2482,7 @@ "required": [ "summary", "lifecycle", - "turns" + "chats" ] }, "SessionActiveClient": { @@ -2392,6 +2537,7 @@ }, "SessionSummary": { "type": "object", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `model` / `agent`: the session-level selection. Per-chat overrides are\n surfaced on individual {@link ChatSummary} entries, not aggregated up.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "resource": { "$ref": "#/$defs/URI", @@ -2435,7 +2581,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory URI for this session" + "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, "changes": { "$ref": "#/$defs/ChangesSummary", @@ -2619,1570 +2765,1774 @@ "values" ] }, - "SessionInputOption": { + "ToolDefinition": { "type": "object", - "description": "A choice in a select-style question.", + "description": "Describes a tool available in a session, provided by either the server or the active client.", "properties": { - "id": { + "name": { "type": "string", - "description": "Stable option identifier; for MCP enum values this is the enum string" + "description": "Unique tool identifier" }, - "label": { + "title": { "type": "string", - "description": "Display label" + "description": "Human-readable display name" }, "description": { "type": "string", - "description": "Optional secondary text" + "description": "Description of what the tool does" }, - "recommended": { - "type": "boolean", - "description": "Whether this option is the recommended/default choice" + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + }, + "outputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + }, + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Behavioral hints about the tool. All properties are advisory." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "id", - "label" + "name" ] }, - "SessionInputQuestionBase": { + "ToolAnnotations": { "type": "object", + "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, "title": { "type": "string", - "description": "Short display title" + "description": "Alternate human-readable title" }, - "message": { - "type": "string", - "description": "Prompt shown to the user" + "readOnlyHint": { + "type": "boolean", + "description": "Tool does not modify its environment (default: false)" }, - "required": { + "destructiveHint": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Tool may perform destructive updates (default: true)" + }, + "idempotentHint": { + "type": "boolean", + "description": "Repeated calls with the same arguments have no additional effect (default: false)" + }, + "openWorldHint": { + "type": "boolean", + "description": "Tool may interact with external entities (default: true)" } - }, - "required": [ - "id", - "message" - ] + } }, - "SessionInputTextQuestion": { + "CustomizationBase": { "type": "object", - "description": "Text question within a session input request.", + "description": "Fields shared by every customization variant.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Text" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "format": { + "name": { "type": "string", - "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" - }, - "min": { - "type": "number", - "description": "Minimum string length" + "description": "Human-readable name." }, - "max": { - "type": "number", - "description": "Maximum string length" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "defaultValue": { - "type": "string", - "description": "Default text" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." } }, "required": [ "id", - "message", - "kind" + "uri", + "name" ] }, - "SessionInputNumberQuestion": { + "CustomizationLoadingState": { "type": "object", - "description": "Numeric question within a session input request.", + "description": "Container is being loaded by the host.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputQuestionKind.Number" - }, - { - "$ref": "#/$defs/SessionInputQuestionKind.Integer" - } - ] - }, - "min": { - "type": "number", - "description": "Minimum value" - }, - "max": { - "type": "number", - "description": "Maximum value" - }, - "defaultValue": { - "type": "number", - "description": "Default numeric value" + "$ref": "#/$defs/CustomizationLoadStatus.Loading" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputBooleanQuestion": { + "CustomizationLoadedState": { "type": "object", - "description": "Boolean question within a session input request.", + "description": "Container loaded successfully.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Boolean" - }, - "defaultValue": { - "type": "boolean", - "description": "Default boolean value" + "$ref": "#/$defs/CustomizationLoadStatus.Loaded" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputSingleSelectQuestion": { + "CustomizationDegradedState": { "type": "object", - "description": "Single-select question within a session input request.", + "description": "Container partially loaded but has warnings.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" + "kind": { + "$ref": "#/$defs/CustomizationLoadStatus.Degraded" }, "message": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.SingleSelect" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputOption" - }, - "description": "Options the user may select from" - }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text instead of selecting an option" + "description": "Human-readable description of the warning." } }, "required": [ - "id", - "message", "kind", - "options" + "message" ] }, - "SessionInputMultiSelectQuestion": { + "CustomizationErrorState": { "type": "object", - "description": "Multi-select question within a session input request.", + "description": "Container failed to load.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" + "kind": { + "$ref": "#/$defs/CustomizationLoadStatus.Error" }, "message": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.MultiSelect" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputOption" - }, - "description": "Options the user may select from" - }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text in addition to selecting options" - }, - "min": { - "type": "number", - "description": "Minimum selected item count" - }, - "max": { - "type": "number", - "description": "Maximum selected item count" + "description": "Human-readable error message." } }, "required": [ - "id", - "message", "kind", - "options" + "message" ] }, - "SessionInputRequest": { + "ContainerCustomizationBase": { "type": "object", - "description": "A live request for user input.\n\nThe server creates or replaces requests with `session/inputRequested`.\nClients sync drafts with `session/inputAnswerChanged` and complete requests\nwith `session/inputCompleted`.", + "description": "Fields shared by container customizations.", "properties": { "id": { "type": "string", - "description": "Stable request identifier" - }, - "message": { - "type": "string", - "description": "Display message for the request as a whole" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "url": { + "uri": { "$ref": "#/$defs/URI", - "description": "URL the user should review or open, for URL-style elicitations" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "questions": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputQuestion" - }, - "description": "Ordered questions to ask the user" - }, - "answers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/SessionInputAnswer" + "$ref": "#/$defs/Icon" }, - "description": "Current draft or submitted answers, keyed by question ID" - } - }, - "required": [ - "id" - ] - }, - "SessionInputTextAnswerValue": { - "type": "object", - "description": "Value captured for one answer.", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Text" + "description": "Icons for UI display." }, - "value": { - "type": "string" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputNumberAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Number" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "value": { - "type": "number" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputBooleanAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Boolean" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "value": { - "type": "boolean" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputSelectedAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Selected" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "value": { - "type": "string" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "freeformValues": { + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered instead of selecting an option" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled" ] }, - "SessionInputSelectedManyAnswerValue": { + "PluginCustomization": { "type": "object", + "description": "An [Open Plugins](https://open-plugins.com/) plugin.", "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.SelectedMany" + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "value": { - "type": "array", - "items": { - "type": "string" - } + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "freeformValues": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "description": "Free-form text entered in addition to selected options" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputAnswered": { - "type": "object", - "properties": { - "state": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputAnswerState.Draft" - }, - { - "$ref": "#/$defs/SessionInputAnswerState.Submitted" - } - ], - "description": "Answer state" + "description": "Icons for UI display." }, - "value": { - "$ref": "#/$defs/SessionInputAnswerValue", - "description": "Answer value" - } - }, - "required": [ - "state", - "value" - ] - }, - "SessionInputSkipped": { - "type": "object", - "properties": { - "state": { - "$ref": "#/$defs/SessionInputAnswerState.Skipped", - "description": "Answer state" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "freeformValues": { + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form reason or value captured while skipping, if any" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" } }, "required": [ - "state" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "Turn": { + "ClientPluginCustomization": { "type": "object", - "description": "A completed request/response cycle.", + "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "responseParts": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/Icon" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." - }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "description": "Icons for UI display." }, - "state": { - "$ref": "#/$defs/TurnState", - "description": "How the turn ended" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details if state is `'error'`" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" + }, + "nonce": { + "type": "string", + "description": "Opaque version token used by the host to detect changes." } }, "required": [ "id", - "message", - "responseParts", - "usage", - "state" + "uri", + "name", + "enabled", + "type" ] }, - "ActiveTurn": { + "DirectoryCustomization": { "type": "object", - "description": "An in-progress turn — the assistant is actively streaming.", + "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "responseParts": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/Icon" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + "description": "Icons for UI display." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Directory" + }, + "contents": { + "$ref": "#/$defs/ChildCustomizationType", + "description": "Which child customization type this directory holds." + }, + "writable": { + "type": "boolean", + "description": "Whether clients may write into this directory." } }, "required": [ "id", - "message", - "responseParts", - "usage" + "uri", + "name", + "enabled", + "type", + "contents", + "writable" ] }, - "Message": { + "AgentCustomization": { "type": "object", - "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", "properties": { - "text": { + "id": { "type": "string", - "description": "Message text" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "origin": { - "type": "object", - "properties": { - "kind": { - "type": "string" - } - }, - "required": [ - "kind" - ], - "description": "The origin of the message" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "attachments": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/MessageAttachment" + "$ref": "#/$defs/Icon" }, - "description": "File/selection attachments" + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Agent" + }, + "description": { + "type": "string", + "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." + "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "text", - "origin" + "id", + "uri", + "name", + "type" ] }, - "MessageAttachmentBase": { + "SkillCustomization": { "type": "object", - "description": "Common fields shared by all {@link MessageAttachment} variants.", + "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, "range": { "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "displayKind": { + "type": { + "$ref": "#/$defs/CustomizationType.Skill" + }, + "description": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." } }, "required": [ - "label" + "id", + "uri", + "name", + "type" ] }, - "SimpleMessageAttachment": { + "PromptCustomization": { "type": "object", - "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", + "description": "A prompt contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "type": { - "$ref": "#/$defs/MessageAttachmentKind.Simple", - "description": "Discriminant" + "$ref": "#/$defs/CustomizationType.Prompt" }, - "modelRepresentation": { + "description": { "type": "string", - "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + "description": "Short description of what the prompt does." } }, "required": [ - "label", + "id", + "uri", + "name", "type" ] }, - "MessageEmbeddedResourceAttachment": { + "RuleCustomization": { "type": "object", - "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "type": { - "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", - "description": "Discriminant" + "$ref": "#/$defs/CustomizationType.Rule" }, - "data": { + "description": { "type": "string", - "description": "Base64-encoded binary data" + "description": "Description of what the rule enforces." }, - "contentType": { - "type": "string", - "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "alwaysApply": { + "type": "boolean", + "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + "globs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." } }, "required": [ - "label", - "type", - "data", - "contentType" + "id", + "uri", + "name", + "type" ] }, - "MessageResourceAttachment": { + "HookCustomization": { "type": "object", - "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", + "description": "A hook manifest contributed by a plugin or directory.", "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." - }, - "displayKind": { + "id": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, "uri": { "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "contentType": { + "name": { "type": "string", - "description": "Content MIME type" + "description": "Human-readable name." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Resource", - "description": "Discriminant" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Hook" } }, "required": [ - "label", + "id", "uri", + "name", "type" ] }, - "MessageAnnotationsAttachment": { + "McpServerCustomization": { "type": "object", - "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "type": { - "$ref": "#/$defs/MessageAttachmentKind.Annotations", - "description": "Discriminant" + "$ref": "#/$defs/CustomizationType.McpServer" }, - "resource": { + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." + }, + "channel": { "$ref": "#/$defs/URI", - "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." }, - "annotationIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ - "label", + "id", + "uri", + "name", "type", - "resource" + "enabled", + "state" ] }, - "MarkdownResponsePart": { + "McpServerCustomizationApps": { "type": "object", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.Markdown", - "description": "Discriminant" - }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/delta` to target this part for content appends" - }, - "content": { - "type": "string", - "description": "Markdown content" + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." } }, "required": [ - "kind", - "id", - "content" + "capabilities" ] }, - "ResourceReponsePart": { + "AhpMcpUiHostCapabilities": { "type": "object", - "description": "A content part that's a reference to large content stored outside the state tree.", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." + } + } + }, + "McpServerStartingState": { + "type": "object", + "description": "Server is registered with the host but has not yet started.", + "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.ContentRef", - "description": "Discriminant" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ - "uri", "kind" ] }, - "ToolCallResponsePart": { + "McpServerReadyState": { "type": "object", - "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", + "description": "Server is running and serving requests.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.ToolCall", - "description": "Discriminant" - }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "Full tool call lifecycle state" + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ - "kind", - "toolCall" + "kind" ] }, - "ReasoningResponsePart": { + "McpServerAuthRequiredState": { "type": "object", - "description": "Reasoning/thinking content from the model.", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.Reasoning", - "description": "Discriminant" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/reasoning` to target this part for content appends" + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." }, - "content": { + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { "type": "string", - "description": "Accumulated reasoning text" + "description": "Human-readable hint, typically from the OAuth `error_description`." } }, "required": [ "kind", - "id", - "content" + "reason", + "resource" ] }, - "SystemNotificationResponsePart": { + "McpServerErrorState": { "type": "object", - "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.SystemNotification", - "description": "Discriminant" + "$ref": "#/$defs/McpServerStatus.Error" }, - "content": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "The text of the system notification" + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." } }, "required": [ "kind", - "content" + "error" ] }, - "ConfirmationOption": { + "McpServerStoppedState": { "type": "object", - "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the option, returned in the confirmed action" - }, - "label": { - "type": "string", - "description": "Human-readable label displayed to the user" - }, "kind": { - "$ref": "#/$defs/ConfirmationOptionKind", - "description": "Whether this option represents an approval or denial" - }, - "group": { - "type": "number", - "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ - "id", - "label", "kind" ] }, - "ToolCallClientContributor": { + "ChatState": { "type": "object", + "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallContributorKind.Client" + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" }, - "clientId": { + "title": { "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." - } - }, - "required": [ - "kind", - "clientId" - ] - }, - "ToolCallMcpContributor": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/ToolCallContributorKind.MCP" + "description": "Chat title" }, - "customizationId": { + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { "type": "string", - "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + "description": "Human-readable description of what the chat is currently doing" + }, + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + }, + "turns": { + "type": "array", + "items": { + "$ref": "#/$defs/Turn" + }, + "description": "Completed turns" + }, + "activeTurn": { + "$ref": "#/$defs/ActiveTurn", + "description": "Currently in-progress turn" + }, + "steeringMessage": { + "$ref": "#/$defs/PendingMessage", + "description": "Message to inject into the current turn at a convenient point" + }, + "queuedMessages": { + "type": "array", + "items": { + "$ref": "#/$defs/PendingMessage" + }, + "description": "Messages to send automatically as new turns after the current turn finishes" + }, + "inputRequests": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputRequest" + }, + "description": "Requests for user input that are currently blocking or informing chat progress" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this chat." } }, "required": [ - "kind", - "customizationId" + "resource", + "title", + "status", + "modifiedAt", + "turns" ] }, - "ToolCallBase": { + "ChatSummary": { "type": "object", - "description": "Metadata common to all tool call states.", + "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", "properties": { - "toolCallId": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" + }, + "title": { "type": "string", - "description": "Unique tool call identifier" + "description": "Chat title" }, - "toolName": { + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Human-readable description of what the chat is currently doing" }, - "displayName": { + "modifiedAt": { "type": "string", - "description": "Human-readable tool name" + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." } }, "required": [ - "toolCallId", - "toolName", - "displayName" + "resource", + "title", + "status", + "modifiedAt" ] }, - "ToolCallParameterFields": { + "PendingMessage": { "type": "object", - "description": "Properties available once tool call parameters are fully received.", + "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", "properties": { - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" - }, - "toolInput": { + "id": { "type": "string", - "description": "Raw tool input" + "description": "Unique identifier for this pending message" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "The message that will start the next turn" } }, "required": [ - "invocationMessage" + "id", + "message" ] }, - "ToolCallResult": { + "ChatInputOption": { "type": "object", - "description": "Tool execution result details, available after execution completes.", + "description": "A choice in a select-style question.", "properties": { - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" - }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "id": { + "type": "string", + "description": "Stable option identifier; for MCP enum values this is the enum string" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "label": { + "type": "string", + "description": "Display label" }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "description": { + "type": "string", + "description": "Optional secondary text" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "recommended": { + "type": "boolean", + "description": "Whether this option is the recommended/default choice" } }, "required": [ - "success", - "pastTenseMessage" + "id", + "label" ] }, - "ToolCallStreamingState": { + "ChatInputQuestionBase": { "type": "object", - "description": "LM is streaming the tool call parameters.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Streaming" - }, - "partialInput": { - "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Prompt shown to the user" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Progress message shown while parameters are streaming" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "status" + "id", + "message" ] }, - "ToolCallPendingConfirmationState": { + "ChatInputTextQuestion": { "type": "object", - "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "description": "Text question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" + "description": "Prompt shown to the user" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Text" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "format": { + "type": "string", + "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" }, - "toolInput": { + "min": { + "type": "number", + "description": "Minimum string length" + }, + "max": { + "type": "number", + "description": "Maximum string length" + }, + "defaultValue": { "type": "string", - "description": "Raw tool input" + "description": "Default text" + } + }, + "required": [ + "id", + "message", + "kind" + ] + }, + "ChatInputNumberQuestion": { + "type": "object", + "description": "Numeric question within a chat input request.", + "properties": { + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + "title": { + "type": "string", + "description": "Short display title" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "string" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" + }, + "kind": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputQuestionKind.Number" + }, + { + "$ref": "#/$defs/ChatInputQuestionKind.Integer" } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" + ] }, - "editable": { - "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + "min": { + "type": "number", + "description": "Minimum value" }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ConfirmationOption" - }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "max": { + "type": "number", + "description": "Maximum value" + }, + "defaultValue": { + "type": "number", + "description": "Default numeric value" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status" + "id", + "message", + "kind" ] }, - "ToolCallRunningState": { + "ChatInputBooleanQuestion": { "type": "object", - "description": "Tool is actively executing.", + "description": "Boolean question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" + "description": "Prompt shown to the user" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Boolean" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "defaultValue": { + "type": "boolean", + "description": "Default boolean value" + } + }, + "required": [ + "id", + "message", + "kind" + ] + }, + "ChatInputSingleSelectQuestion": { + "type": "object", + "description": "Single-select question within a chat input request.", + "properties": { + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "toolInput": { + "title": { "type": "string", - "description": "Raw tool input" + "description": "Short display title" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Running" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.SingleSelect" }, - "content": { + "options": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChatInputOption" }, - "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + "description": "Options the user may select from" + }, + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text instead of selecting an option" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "confirmed" + "id", + "message", + "kind", + "options" ] }, - "ToolCallPendingResultConfirmationState": { + "ChatInputMultiSelectQuestion": { "type": "object", - "description": "Tool finished executing, waiting for client to approve the result.", + "description": "Multi-select question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "description": "Prompt shown to the user" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.MultiSelect" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputOption" + }, + "description": "Options the user may select from" }, - "success": { + "allowFreeformInput": { "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Whether the user may enter text in addition to selecting options" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "min": { + "type": "number", + "description": "Minimum selected item count" }, - "content": { + "max": { + "type": "number", + "description": "Maximum selected item count" + } + }, + "required": [ + "id", + "message", + "kind", + "options" + ] + }, + "ChatInputRequest": { + "type": "object", + "description": "A live request for user input.\n\nThe server creates or replaces requests with `chat/inputRequested`.\nClients sync drafts with `chat/inputAnswerChanged` and complete requests\nwith `chat/inputCompleted`.", + "properties": { + "id": { + "type": "string", + "description": "Stable request identifier" + }, + "message": { + "type": "string", + "description": "Display message for the request as a whole" + }, + "url": { + "$ref": "#/$defs/URI", + "description": "URL the user should review or open, for URL-style elicitations" + }, + "questions": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChatInputQuestion" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "description": "Ordered questions to ask the user" }, - "structuredContent": { + "answers": { "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "additionalProperties": { + "$ref": "#/$defs/ChatInputAnswer" + }, + "description": "Current draft or submitted answers, keyed by question ID" + } + }, + "required": [ + "id" + ] + }, + "ChatInputTextAnswerValue": { + "type": "object", + "description": "Value captured for one answer.", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Text" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputNumberAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Number" + }, + "value": { + "type": "number" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputBooleanAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Boolean" + }, + "value": { + "type": "boolean" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputSelectedAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Selected" + }, + "value": { + "type": "string" + }, + "freeformValues": { + "type": "array", + "items": { + "type": "string" }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "description": "Free-form text entered instead of selecting an option" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputSelectedManyAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.SelectedMany" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" + "value": { + "type": "array", + "items": { + "type": "string" + } }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered in addition to selected options" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputAnswered": { + "type": "object", + "properties": { + "state": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputAnswerState.Draft" + }, + { + "$ref": "#/$defs/ChatInputAnswerState.Submitted" + } + ], + "description": "Answer state" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "value": { + "$ref": "#/$defs/ChatInputAnswerValue", + "description": "Answer value" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "state", + "value" ] }, - "ToolCallCompletedState": { + "ChatInputSkipped": { "type": "object", - "description": "Tool completed successfully or with an error.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" + "state": { + "$ref": "#/$defs/ChatInputAnswerState.Skipped", + "description": "Answer state" }, - "toolName": { + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form reason or value captured while skipping, if any" + } + }, + "required": [ + "state" + ] + }, + "Turn": { + "type": "object", + "description": "A completed request/response cycle.", + "properties": { + "id": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Turn identifier" }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "state": { + "$ref": "#/$defs/TurnState", + "description": "How the turn ended" }, - "toolInput": { + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details if state is `'error'`" + } + }, + "required": [ + "id", + "message", + "responseParts", + "usage", + "state" + ] + }, + "ActiveTurn": { + "type": "object", + "description": "An in-progress turn — the assistant is actively streaming.", + "properties": { + "id": { "type": "string", - "description": "Raw tool input" - }, - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Turn identifier" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "content": { + "responseParts": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ResponsePart" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" + } + }, + "required": [ + "id", + "message", + "responseParts", + "usage" + ] + }, + "Message": { + "type": "object", + "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "properties": { + "text": { + "type": "string", + "description": "Message text" }, - "error": { + "origin": { "type": "object", "properties": { - "message": { - "type": "string" - }, - "code": { + "kind": { "type": "string" } }, "required": [ - "message" + "kind" ], - "description": "Error details if the tool failed" + "description": "The origin of the message" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Completed" + "attachments": { + "type": "array", + "items": { + "$ref": "#/$defs/MessageAttachment" + }, + "description": "File/selection attachments" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." + } + }, + "required": [ + "text", + "origin" + ] + }, + "MessageAttachmentBase": { + "type": "object", + "description": "Common fields shared by all {@link MessageAttachment} variants.", + "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "label" ] }, - "ToolCallCancelledState": { + "SimpleMessageAttachment": { "type": "object", - "description": "Tool call was cancelled before execution.", + "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", "properties": { - "toolCallId": { + "label": { "type": "string", - "description": "Unique tool call identifier" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "displayName": { + "displayKind": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Simple", + "description": "Discriminant" }, - "toolInput": { + "modelRepresentation": { "type": "string", - "description": "Raw tool input" + "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + } + }, + "required": [ + "label", + "type" + ] + }, + "MessageEmbeddedResourceAttachment": { + "type": "object", + "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Cancelled" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "reason": { - "$ref": "#/$defs/ToolCallCancellationReason", - "description": "Why the tool was cancelled" + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional message explaining the cancellation" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" + "type": { + "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", + "description": "Discriminant" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "data": { + "type": "string", + "description": "Base64-encoded binary data" + }, + "contentType": { + "type": "string", + "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "reason" + "label", + "type", + "data", + "contentType" ] }, - "ToolDefinition": { + "MessageResourceAttachment": { "type": "object", - "description": "Describes a tool available in a session, provided by either the server or the active client.", + "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", "properties": { - "name": { + "label": { "type": "string", - "description": "Unique tool identifier" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "title": { - "type": "string", - "description": "Human-readable display name" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "description": { + "displayKind": { "type": "string", - "description": "Description of what the tool does" + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "inputSchema": { + "_meta": { "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } - }, - "required": [ - "type" - ], - "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "outputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } - }, - "required": [ - "type" - ], - "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" }, - "annotations": { - "$ref": "#/$defs/ToolAnnotations", - "description": "Behavioral hints about the tool. All properties are advisory." + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Resource", + "description": "Discriminant" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "name" + "label", + "uri", + "type" ] }, - "ToolAnnotations": { + "MessageAnnotationsAttachment": { "type": "object", - "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", + "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", "properties": { - "title": { + "label": { "type": "string", - "description": "Alternate human-readable title" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "readOnlyHint": { - "type": "boolean", - "description": "Tool does not modify its environment (default: false)" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "destructiveHint": { - "type": "boolean", - "description": "Tool may perform destructive updates (default: true)" + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "idempotentHint": { - "type": "boolean", - "description": "Repeated calls with the same arguments have no additional effect (default: false)" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "openWorldHint": { - "type": "boolean", - "description": "Tool may interact with external entities (default: true)" - } - } - }, - "ToolResultTextContent": { - "type": "object", - "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", - "properties": { "type": { - "$ref": "#/$defs/ToolResultContentType.Text" + "$ref": "#/$defs/MessageAttachmentKind.Annotations", + "description": "Discriminant" }, - "text": { - "type": "string", - "description": "The text content" + "resource": { + "$ref": "#/$defs/URI", + "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + }, + "annotationIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." } }, "required": [ + "label", "type", - "text" + "resource" ] }, - "ToolResultEmbeddedResourceContent": { + "MarkdownResponsePart": { "type": "object", - "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" + "kind": { + "$ref": "#/$defs/ResponsePartKind.Markdown", + "description": "Discriminant" }, - "data": { + "id": { "type": "string", - "description": "Base64-encoded data" + "description": "Part identifier, used by `chat/delta` to target this part for content appends" }, - "contentType": { + "content": { "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "description": "Markdown content" } }, "required": [ - "type", - "data", - "contentType" + "kind", + "id", + "content" ] }, - "ToolResultResourceContent": { + "ResourceReponsePart": { "type": "object", - "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", + "description": "A content part that's a reference to large content stored outside the state tree.", "properties": { "uri": { "$ref": "#/$defs/URI", @@ -4196,874 +4546,819 @@ "type": "string", "description": "Content MIME type" }, - "type": { - "$ref": "#/$defs/ToolResultContentType.Resource" + "kind": { + "$ref": "#/$defs/ResponsePartKind.ContentRef", + "description": "Discriminant" } }, "required": [ "uri", - "type" + "kind" ] }, - "ToolResultFileEditContent": { + "ToolCallResponsePart": { "type": "object", - "description": "Describes a file modification performed by a tool.", + "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", "properties": { - "before": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." - }, - "after": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." - }, - "diff": { - "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } - }, - "description": "Optional diff display metadata" + "kind": { + "$ref": "#/$defs/ResponsePartKind.ToolCall", + "description": "Discriminant" }, - "type": { - "$ref": "#/$defs/ToolResultContentType.FileEdit" + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "Full tool call lifecycle state" } }, "required": [ - "type" + "kind", + "toolCall" ] }, - "ToolResultTerminalContent": { + "ReasoningResponsePart": { "type": "object", - "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", + "description": "Reasoning/thinking content from the model.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Terminal" + "kind": { + "$ref": "#/$defs/ResponsePartKind.Reasoning", + "description": "Discriminant" }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "id": { + "type": "string", + "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" }, - "title": { + "content": { "type": "string", - "description": "Display title for the terminal content" + "description": "Accumulated reasoning text" } }, "required": [ - "type", - "resource", - "title" + "kind", + "id", + "content" ] }, - "ToolResultSubagentContent": { + "SystemNotificationResponsePart": { "type": "object", - "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", + "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Subagent" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Subagent session URI (subscribable for full session state)" - }, - "title": { - "type": "string", - "description": "Display title for the subagent" - }, - "agentName": { - "type": "string", - "description": "Internal agent name" + "kind": { + "$ref": "#/$defs/ResponsePartKind.SystemNotification", + "description": "Discriminant" }, - "description": { - "type": "string", - "description": "Human-readable description of the subagent's task" + "content": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "The text of the system notification" } }, "required": [ - "type", - "resource", - "title" + "kind", + "content" ] }, - "CustomizationBase": { + "ConfirmationOption": { "type": "object", - "description": "Fields shared by every customization variant.", + "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", "properties": { "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Unique identifier for the option, returned in the confirmed action" }, - "name": { + "label": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable label displayed to the user" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "kind": { + "$ref": "#/$defs/ConfirmationOptionKind", + "description": "Whether this option represents an approval or denial" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "group": { + "type": "number", + "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." } }, "required": [ "id", - "uri", - "name" + "label", + "kind" ] }, - "CustomizationLoadingState": { + "ToolCallClientContributor": { "type": "object", - "description": "Container is being loaded by the host.", "properties": { "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loading" + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." } }, "required": [ - "kind" + "kind", + "clientId" ] }, - "CustomizationLoadedState": { + "ToolCallMcpContributor": { "type": "object", - "description": "Container loaded successfully.", "properties": { "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loaded" + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." } }, "required": [ - "kind" + "kind", + "customizationId" ] }, - "CustomizationDegradedState": { + "ToolCallBase": { "type": "object", - "description": "Container partially loaded but has warnings.", + "description": "Metadata common to all tool call states.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Degraded" + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" }, - "message": { + "toolName": { "type": "string", - "description": "Human-readable description of the warning." + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." } }, "required": [ - "kind", - "message" + "toolCallId", + "toolName", + "displayName" ] }, - "CustomizationErrorState": { + "ToolCallParameterFields": { "type": "object", - "description": "Container failed to load.", + "description": "Properties available once tool call parameters are fully received.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Error" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "message": { + "toolInput": { "type": "string", - "description": "Human-readable error message." + "description": "Raw tool input" } }, "required": [ - "kind", - "message" + "invocationMessage" ] }, - "ContainerCustomizationBase": { + "ToolCallResult": { "type": "object", - "description": "Fields shared by container customizations.", + "description": "Tool execution result details, available after execution completes.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" }, - "name": { - "type": "string", - "description": "Human-readable name." + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" }, - "icons": { + "content": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, - "clientId": { - "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "required": [ + "message" + ], + "description": "Error details if the tool failed" } }, "required": [ - "id", - "uri", - "name", - "enabled" + "success", + "pastTenseMessage" ] }, - "PluginCustomization": { + "ToolCallStreamingState": { "type": "object", - "description": "An [Open Plugins](https://open-plugins.com/) plugin.", + "description": "LM is streaming the tool call parameters.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "status": { + "$ref": "#/$defs/ToolCallStatus.Streaming" }, - "clientId": { + "partialInput": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "description": "Partial parameters accumulated so far" }, - "type": { - "$ref": "#/$defs/CustomizationType.Plugin" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Progress message shown while parameters are streaming" } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "toolCallId", + "toolName", + "displayName", + "status" ] }, - "ClientPluginCustomization": { + "ToolCallPendingConfirmationState": { "type": "object", - "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", + "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "clientId": { + "toolInput": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Raw tool input" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "edits": { + "type": "object", + "properties": { + "items": { + "type": "string" + } }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" }, - "type": { - "$ref": "#/$defs/CustomizationType.Plugin" + "editable": { + "type": "boolean", + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" }, - "nonce": { - "type": "string", - "description": "Opaque version token used by the host to detect changes." + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfirmationOption" + }, + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status" ] }, - "DirectoryCustomization": { + "ToolCallRunningState": { "type": "object", - "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", + "description": "Tool is actively executing.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Running" }, - "clientId": { - "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" }, - "children": { + "content": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Directory" - }, - "contents": { - "$ref": "#/$defs/ChildCustomizationType", - "description": "Which child customization type this directory holds." - }, - "writable": { - "type": "boolean", - "description": "Whether clients may write into this directory." + "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type", - "contents", - "writable" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "confirmed" ] }, - "AgentCustomization": { + "ToolCallPendingResultConfirmationState": { "type": "object", - "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", + "description": "Tool finished executing, waiting for client to approve the result.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Unique tool call identifier" }, - "name": { + "toolName": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Agent" + "description": "Internal tool name (for debugging/logging)" }, - "description": { + "displayName": { "type": "string", - "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." - } - }, - "required": [ - "id", - "uri", - "name", - "type" - ] - }, - "SkillCustomization": { - "type": "object", - "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", - "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "name": { + "toolInput": { "type": "string", - "description": "Human-readable name." + "description": "Raw tool input" }, - "icons": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Icons for UI display." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "type": { - "$ref": "#/$defs/CustomizationType.Skill" + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "description": { - "type": "string", - "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" }, - "disableModelInvocation": { - "type": "boolean", - "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "PromptCustomization": { + "ToolCallCompletedState": { "type": "object", - "description": "A prompt contributed by a plugin or directory.", + "description": "Tool completed successfully or with an error.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Icons for UI display." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "type": { - "$ref": "#/$defs/CustomizationType.Prompt" + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "description": { - "type": "string", - "description": "Short description of what the prompt does." + "status": { + "$ref": "#/$defs/ToolCallStatus.Completed" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "RuleCustomization": { + "ToolCallCancelledState": { "type": "object", - "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", + "description": "Tool call was cancelled before execution.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "$ref": "#/$defs/CustomizationType.Rule" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "description": { + "toolInput": { "type": "string", - "description": "Description of what the rule enforces." + "description": "Raw tool input" }, - "alwaysApply": { - "type": "boolean", - "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." + "status": { + "$ref": "#/$defs/ToolCallStatus.Cancelled" }, - "globs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." + "reason": { + "$ref": "#/$defs/ToolCallCancellationReason", + "description": "Why the tool was cancelled" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional message explaining the cancellation" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "reason" ] }, - "HookCustomization": { + "ToolResultTextContent": { "type": "object", - "description": "A hook manifest contributed by a plugin or directory.", + "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "type": { + "$ref": "#/$defs/ToolResultContentType.Text" }, - "name": { + "text": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Hook" + "description": "The text content" } }, "required": [ - "id", - "uri", - "name", - "type" + "type", + "text" ] }, - "McpServerCustomization": { + "ToolResultEmbeddedResourceContent": { "type": "object", - "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", + "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, "type": { - "$ref": "#/$defs/CustomizationType.McpServer" - }, - "enabled": { - "type": "boolean", - "description": "Whether this MCP server is currently enabled." - }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "Current lifecycle state of the MCP server." + "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" }, - "channel": { - "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "data": { + "type": "string", + "description": "Base64-encoded data" }, - "mcpApp": { - "$ref": "#/$defs/McpServerCustomizationApps", - "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" } }, "required": [ - "id", - "uri", - "name", "type", - "enabled", - "state" + "data", + "contentType" ] }, - "McpServerCustomizationApps": { + "ToolResultResourceContent": { "type": "object", - "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "capabilities": { - "$ref": "#/$defs/AhpMcpUiHostCapabilities", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.Resource" } }, "required": [ - "capabilities" + "uri", + "type" ] }, - "AhpMcpUiHostCapabilities": { + "ToolResultFileEditContent": { "type": "object", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "description": "Describes a file modification performed by a tool.", "properties": { - "serverTools": { + "before": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." }, - "serverResources": { + "after": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `resources/*` methods to the upstream server." - }, - "logging": { - "type": "object", - "additionalProperties": {}, - "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." }, - "sampling": { + "diff": { "type": "object", "properties": { - "tools": { - "type": "string" + "added": { + "type": "number" + }, + "removed": { + "type": "number" } }, - "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." - } - } - }, - "McpServerStartingState": { - "type": "object", - "description": "Server is registered with the host but has not yet started.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Starting" + "description": "Optional diff display metadata" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.FileEdit" } }, "required": [ - "kind" + "type" ] }, - "McpServerReadyState": { + "ToolResultTerminalContent": { "type": "object", - "description": "Server is running and serving requests.", + "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Ready" + "type": { + "$ref": "#/$defs/ToolResultContentType.Terminal" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" + }, + "title": { + "type": "string", + "description": "Display title for the terminal content" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, - "McpServerAuthRequiredState": { + "ToolResultSubagentContent": { "type": "object", - "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.AuthRequired" - }, - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "type": { + "$ref": "#/$defs/ToolResultContentType.Subagent" }, "resource": { - "$ref": "#/$defs/ProtectedResourceMetadata", - "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + "$ref": "#/$defs/URI", + "description": "Subagent session URI (subscribable for full session state)" }, - "requiredScopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + "title": { + "type": "string", + "description": "Display title for the subagent" }, - "description": { + "agentName": { "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." - } - }, - "required": [ - "kind", - "reason", - "resource" - ] - }, - "McpServerErrorState": { - "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Error" + "description": "Internal agent name" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details." - } - }, - "required": [ - "kind", - "error" - ] - }, - "McpServerStoppedState": { - "type": "object", - "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Stopped" + "description": { + "type": "string", + "description": "Human-readable description of the subagent's task" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, "TerminalInfo": { @@ -5495,141 +5790,297 @@ "$ref": "#/$defs/URI", "description": "Channel URI (or RFC 6570 URI template) for OTLP log records\n(`otlp/exportLogs` notifications).\n\nThe following template variables are defined by this protocol; any\nother variable name MUST be ignored by clients (there is no\nprotocol-defined way to obtain values for unknown variables):\n\n| Variables in template | Meaning |\n| --------------------- | ------------------------------------------------------------------------------------------------------- |\n| _(none)_ | The host does not support subscriber-side severity filtering. The template is itself a subscribable URI. |\n| `{level}` | Minimum OTLP severity to deliver. Expand to one of the [OTLP `SeverityNumber`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) short names (case-insensitive): `trace`, `debug`, `info`, `warn`, `error`, `fatal`. The server delivers log records whose `severityNumber` falls in the corresponding band or above. |\n\nHosts SHOULD honour the expanded `{level}`; clients MUST still filter\ndefensively in case a host ignores the parameter. Hosts that do not\nadvertise `{level}` deliver all severities.\n\nFuture protocol versions MAY add new well-known variables (e.g. scope\nor attribute filters)." }, - "traces": { - "$ref": "#/$defs/URI", - "description": "Channel URI for OTLP spans (`otlp/exportTraces` notifications). No\ntemplate variables are defined by this protocol version." + "traces": { + "$ref": "#/$defs/URI", + "description": "Channel URI for OTLP spans (`otlp/exportTraces` notifications). No\ntemplate variables are defined by this protocol version." + }, + "metrics": { + "$ref": "#/$defs/URI", + "description": "Channel URI for OTLP metric data points (`otlp/exportMetrics`\nnotifications). No template variables are defined by this protocol\nversion." + } + } + }, + "ResourceWatchState": { + "type": "object", + "description": "Full state for a single resource watch, returned when a client subscribes\nto an `ahp-resource-watch:` URI.\n\nWatches are otherwise stateless: the watcher exists to deliver\n{@link ResourceWatchChangedAction} events. The state carries only the\ndescriptor of what is being watched so a re-subscribing client can\nrecover the watch configuration after reconnecting.", + "properties": { + "root": { + "$ref": "#/$defs/URI", + "description": "The URI being watched. For recursive watches this is the root of the\nsubtree; for non-recursive watches this is the single file or\ndirectory." + }, + "recursive": { + "type": "boolean", + "description": "`true` if the watcher reports changes for descendants of `root`;\n`false` if it only reports changes to `root` itself (and, when\n`root` is a directory, its direct children)." + }, + "excludes": { + "type": "object", + "properties": { + "items": { + "type": "string" + } + }, + "required": [ + "items" + ], + "description": "Optional glob patterns or paths relative to `root` to exclude from\nchange reporting." + }, + "includes": { + "type": "object", + "properties": { + "items": { + "type": "string" + } + }, + "required": [ + "items" + ], + "description": "Optional glob patterns or paths relative to `root` to restrict\nchange reporting to. Omit to report every change under `root`\nsubject to `excludes`." + } + }, + "required": [ + "root", + "recursive" + ] + }, + "ResourceChange": { + "type": "object", + "description": "A single change observed by a resource watcher.", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "The URI of the resource that changed." + }, + "type": { + "$ref": "#/$defs/ResourceChangeType", + "description": "The kind of change observed." + } + }, + "required": [ + "uri", + "type" + ] + }, + "StringOrMarkdown": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "markdown": { + "type": "string" + } + }, + "required": [ + "markdown" + ] + } + ], + "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." + }, + "ChildCustomizationType": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/CustomizationType.Agent" + }, + { + "$ref": "#/$defs/CustomizationType.Skill" + }, + { + "$ref": "#/$defs/CustomizationType.Prompt" + }, + { + "$ref": "#/$defs/CustomizationType.Rule" + }, + { + "$ref": "#/$defs/CustomizationType.Hook" + }, + { + "$ref": "#/$defs/CustomizationType.McpServer" + } + ], + "description": "Customization types that appear as children of a\n{@link PluginCustomization} or {@link DirectoryCustomization}." + }, + "CustomizationLoadState": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/CustomizationLoadingState" + }, + { + "$ref": "#/$defs/CustomizationLoadedState" + }, + { + "$ref": "#/$defs/CustomizationDegradedState" + }, + { + "$ref": "#/$defs/CustomizationErrorState" + } + ], + "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "ChildCustomization": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/AgentCustomization" + }, + { + "$ref": "#/$defs/SkillCustomization" + }, + { + "$ref": "#/$defs/PromptCustomization" + }, + { + "$ref": "#/$defs/RuleCustomization" + }, + { + "$ref": "#/$defs/HookCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." + }, + "Customization": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/PluginCustomization" + }, + { + "$ref": "#/$defs/DirectoryCustomization" }, - "metrics": { - "$ref": "#/$defs/URI", - "description": "Channel URI for OTLP metric data points (`otlp/exportMetrics`\nnotifications). No template variables are defined by this protocol\nversion." + { + "$ref": "#/$defs/McpServerCustomization" } - } + ], + "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." }, - "ResourceWatchState": { - "type": "object", - "description": "Full state for a single resource watch, returned when a client subscribes\nto an `ahp-resource-watch:` URI.\n\nWatches are otherwise stateless: the watcher exists to deliver\n{@link ResourceWatchChangedAction} events. The state carries only the\ndescriptor of what is being watched so a re-subscribing client can\nrecover the watch configuration after reconnecting.", - "properties": { - "root": { - "$ref": "#/$defs/URI", - "description": "The URI being watched. For recursive watches this is the root of the\nsubtree; for non-recursive watches this is the single file or\ndirectory." + "McpServerState": { + "oneOf": [ + {}, + { + "$ref": "#/$defs/McpServerStartingState" }, - "recursive": { - "type": "boolean", - "description": "`true` if the watcher reports changes for descendants of `root`;\n`false` if it only reports changes to `root` itself (and, when\n`root` is a directory, its direct children)." + { + "$ref": "#/$defs/McpServerReadyState" }, - "excludes": { + { + "$ref": "#/$defs/McpServerAuthRequiredState" + }, + { + "$ref": "#/$defs/McpServerErrorState" + }, + { + "$ref": "#/$defs/McpServerStoppedState" + } + ], + "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." + }, + "ChatOrigin": { + "oneOf": [ + {}, + { "type": "object", "properties": { - "items": { + "kind": { "type": "string" } }, "required": [ - "items" - ], - "description": "Optional glob patterns or paths relative to `root` to exclude from\nchange reporting." + "kind" + ] }, - "includes": { + { "type": "object", "properties": { - "items": { + "kind": { + "type": "string" + }, + "chat": { + "type": "string" + }, + "turnId": { "type": "string" } }, "required": [ - "items" - ], - "description": "Optional glob patterns or paths relative to `root` to restrict\nchange reporting to. Omit to report every change under `root`\nsubject to `excludes`." - } - }, - "required": [ - "root", - "recursive" - ] - }, - "ResourceChange": { - "type": "object", - "description": "A single change observed by a resource watcher.", - "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "The URI of the resource that changed." - }, - "type": { - "$ref": "#/$defs/ResourceChangeType", - "description": "The kind of change observed." - } - }, - "required": [ - "uri", - "type" - ] - }, - "StringOrMarkdown": { - "oneOf": [ - { - "type": "string" + "kind", + "chat", + "turnId" + ] }, { "type": "object", "properties": { - "markdown": { + "kind": { + "type": "string" + }, + "chat": { + "type": "string" + }, + "toolCallId": { "type": "string" } }, "required": [ - "markdown" + "kind", + "chat", + "toolCallId" ] } - ], - "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." + ] }, - "SessionInputQuestion": { + "ChatInputQuestion": { "oneOf": [ { - "$ref": "#/$defs/SessionInputTextQuestion" + "$ref": "#/$defs/ChatInputTextQuestion" }, { - "$ref": "#/$defs/SessionInputNumberQuestion" + "$ref": "#/$defs/ChatInputNumberQuestion" }, { - "$ref": "#/$defs/SessionInputBooleanQuestion" + "$ref": "#/$defs/ChatInputBooleanQuestion" }, { - "$ref": "#/$defs/SessionInputSingleSelectQuestion" + "$ref": "#/$defs/ChatInputSingleSelectQuestion" }, { - "$ref": "#/$defs/SessionInputMultiSelectQuestion" + "$ref": "#/$defs/ChatInputMultiSelectQuestion" } ], - "description": "One question within a session input request." + "description": "One question within a chat input request." }, - "SessionInputAnswerValue": { + "ChatInputAnswerValue": { "oneOf": [ { - "$ref": "#/$defs/SessionInputTextAnswerValue" + "$ref": "#/$defs/ChatInputTextAnswerValue" }, { - "$ref": "#/$defs/SessionInputNumberAnswerValue" + "$ref": "#/$defs/ChatInputNumberAnswerValue" }, { - "$ref": "#/$defs/SessionInputBooleanAnswerValue" + "$ref": "#/$defs/ChatInputBooleanAnswerValue" }, { - "$ref": "#/$defs/SessionInputSelectedAnswerValue" + "$ref": "#/$defs/ChatInputSelectedAnswerValue" }, { - "$ref": "#/$defs/SessionInputSelectedManyAnswerValue" + "$ref": "#/$defs/ChatInputSelectedManyAnswerValue" } ] }, - "SessionInputAnswer": { + "ChatInputAnswer": { "oneOf": [ { - "$ref": "#/$defs/SessionInputAnswered" + "$ref": "#/$defs/ChatInputAnswered" }, { - "$ref": "#/$defs/SessionInputSkipped" + "$ref": "#/$defs/ChatInputSkipped" } ], "description": "Draft, submitted, or skipped answer for one question." @@ -5730,108 +6181,6 @@ ], "description": "Content block in a tool result.\n\nMirrors the content blocks in MCP `CallToolResult.content`, plus\n`ToolResultResourceContent` for lazy-loading large results,\n`ToolResultFileEditContent` for file edit diffs,\n`ToolResultTerminalContent` for live terminal output, and\n`ToolResultSubagentContent` for subagent sessions (AHP extensions)." }, - "ChildCustomizationType": { - "oneOf": [ - {}, - { - "$ref": "#/$defs/CustomizationType.Agent" - }, - { - "$ref": "#/$defs/CustomizationType.Skill" - }, - { - "$ref": "#/$defs/CustomizationType.Prompt" - }, - { - "$ref": "#/$defs/CustomizationType.Rule" - }, - { - "$ref": "#/$defs/CustomizationType.Hook" - }, - { - "$ref": "#/$defs/CustomizationType.McpServer" - } - ], - "description": "Customization types that appear as children of a\n{@link PluginCustomization} or {@link DirectoryCustomization}." - }, - "CustomizationLoadState": { - "oneOf": [ - {}, - { - "$ref": "#/$defs/CustomizationLoadingState" - }, - { - "$ref": "#/$defs/CustomizationLoadedState" - }, - { - "$ref": "#/$defs/CustomizationDegradedState" - }, - { - "$ref": "#/$defs/CustomizationErrorState" - } - ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." - }, - "ChildCustomization": { - "oneOf": [ - {}, - { - "$ref": "#/$defs/AgentCustomization" - }, - { - "$ref": "#/$defs/SkillCustomization" - }, - { - "$ref": "#/$defs/PromptCustomization" - }, - { - "$ref": "#/$defs/RuleCustomization" - }, - { - "$ref": "#/$defs/HookCustomization" - }, - { - "$ref": "#/$defs/McpServerCustomization" - } - ], - "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." - }, - "Customization": { - "oneOf": [ - {}, - { - "$ref": "#/$defs/PluginCustomization" - }, - { - "$ref": "#/$defs/DirectoryCustomization" - }, - { - "$ref": "#/$defs/McpServerCustomization" - } - ], - "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." - }, - "McpServerState": { - "oneOf": [ - {}, - { - "$ref": "#/$defs/McpServerStartingState" - }, - { - "$ref": "#/$defs/McpServerReadyState" - }, - { - "$ref": "#/$defs/McpServerAuthRequiredState" - }, - { - "$ref": "#/$defs/McpServerErrorState" - }, - { - "$ref": "#/$defs/McpServerStoppedState" - } - ], - "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." - }, "TerminalClaim": { "oneOf": [ { @@ -5880,121 +6229,133 @@ "$ref": "#/$defs/SessionCreationFailedAction" }, { - "$ref": "#/$defs/SessionTurnStartedAction" + "$ref": "#/$defs/SessionChatAddedAction" }, { - "$ref": "#/$defs/SessionDeltaAction" + "$ref": "#/$defs/SessionChatRemovedAction" }, { - "$ref": "#/$defs/SessionResponsePartAction" + "$ref": "#/$defs/SessionChatUpdatedAction" }, { - "$ref": "#/$defs/SessionToolCallStartAction" + "$ref": "#/$defs/SessionDefaultChatChangedAction" }, { - "$ref": "#/$defs/SessionToolCallDeltaAction" + "$ref": "#/$defs/SessionTitleChangedAction" }, { - "$ref": "#/$defs/SessionToolCallReadyAction" + "$ref": "#/$defs/SessionModelChangedAction" }, { - "$ref": "#/$defs/SessionToolCallConfirmedAction" + "$ref": "#/$defs/SessionAgentChangedAction" }, { - "$ref": "#/$defs/SessionToolCallCompleteAction" + "$ref": "#/$defs/SessionServerToolsChangedAction" }, { - "$ref": "#/$defs/SessionToolCallResultConfirmedAction" + "$ref": "#/$defs/SessionActiveClientChangedAction" }, { - "$ref": "#/$defs/SessionToolCallContentChangedAction" + "$ref": "#/$defs/SessionActiveClientToolsChangedAction" }, { - "$ref": "#/$defs/SessionTurnCompleteAction" + "$ref": "#/$defs/SessionCustomizationsChangedAction" }, { - "$ref": "#/$defs/SessionTurnCancelledAction" + "$ref": "#/$defs/SessionCustomizationToggledAction" }, { - "$ref": "#/$defs/SessionErrorAction" + "$ref": "#/$defs/SessionCustomizationUpdatedAction" }, { - "$ref": "#/$defs/SessionTitleChangedAction" + "$ref": "#/$defs/SessionCustomizationRemovedAction" }, { - "$ref": "#/$defs/SessionUsageAction" + "$ref": "#/$defs/SessionMcpServerStateChangedAction" }, { - "$ref": "#/$defs/SessionReasoningAction" + "$ref": "#/$defs/SessionIsReadChangedAction" }, { - "$ref": "#/$defs/SessionModelChangedAction" + "$ref": "#/$defs/SessionIsArchivedChangedAction" }, { - "$ref": "#/$defs/SessionAgentChangedAction" + "$ref": "#/$defs/SessionActivityChangedAction" }, { - "$ref": "#/$defs/SessionServerToolsChangedAction" + "$ref": "#/$defs/SessionChangesetsChangedAction" }, { - "$ref": "#/$defs/SessionActiveClientChangedAction" + "$ref": "#/$defs/SessionConfigChangedAction" }, { - "$ref": "#/$defs/SessionActiveClientToolsChangedAction" + "$ref": "#/$defs/SessionMetaChangedAction" }, { - "$ref": "#/$defs/SessionPendingMessageSetAction" + "$ref": "#/$defs/ChatTurnStartedAction" }, { - "$ref": "#/$defs/SessionPendingMessageRemovedAction" + "$ref": "#/$defs/ChatDeltaAction" }, { - "$ref": "#/$defs/SessionQueuedMessagesReorderedAction" + "$ref": "#/$defs/ChatResponsePartAction" }, { - "$ref": "#/$defs/SessionInputRequestedAction" + "$ref": "#/$defs/ChatToolCallStartAction" }, { - "$ref": "#/$defs/SessionInputAnswerChangedAction" + "$ref": "#/$defs/ChatToolCallDeltaAction" }, { - "$ref": "#/$defs/SessionInputCompletedAction" + "$ref": "#/$defs/ChatToolCallReadyAction" }, { - "$ref": "#/$defs/SessionCustomizationsChangedAction" + "$ref": "#/$defs/ChatToolCallConfirmedAction" }, { - "$ref": "#/$defs/SessionCustomizationToggledAction" + "$ref": "#/$defs/ChatToolCallCompleteAction" }, { - "$ref": "#/$defs/SessionCustomizationUpdatedAction" + "$ref": "#/$defs/ChatToolCallResultConfirmedAction" }, { - "$ref": "#/$defs/SessionCustomizationRemovedAction" + "$ref": "#/$defs/ChatToolCallContentChangedAction" }, { - "$ref": "#/$defs/SessionMcpServerStateChangedAction" + "$ref": "#/$defs/ChatTurnCompleteAction" }, { - "$ref": "#/$defs/SessionTruncatedAction" + "$ref": "#/$defs/ChatTurnCancelledAction" }, { - "$ref": "#/$defs/SessionIsReadChangedAction" + "$ref": "#/$defs/ChatErrorAction" }, { - "$ref": "#/$defs/SessionIsArchivedChangedAction" + "$ref": "#/$defs/ChatUsageAction" }, { - "$ref": "#/$defs/SessionActivityChangedAction" + "$ref": "#/$defs/ChatReasoningAction" }, { - "$ref": "#/$defs/SessionChangesetsChangedAction" + "$ref": "#/$defs/ChatPendingMessageSetAction" }, { - "$ref": "#/$defs/SessionConfigChangedAction" + "$ref": "#/$defs/ChatPendingMessageRemovedAction" }, { - "$ref": "#/$defs/SessionMetaChangedAction" + "$ref": "#/$defs/ChatQueuedMessagesReorderedAction" + }, + { + "$ref": "#/$defs/ChatInputRequestedAction" + }, + { + "$ref": "#/$defs/ChatInputAnswerChangedAction" + }, + { + "$ref": "#/$defs/ChatInputCompletedAction" + }, + { + "$ref": "#/$defs/ChatTruncatedAction" }, { "$ref": "#/$defs/ChangesetStatusChangedAction" diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 6d7a4bdd..a3b0099f 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -907,11 +907,11 @@ }, "FetchTurnsParams": { "type": "object", - "description": "Fetches historical turns for a session. Used for lazy loading of conversation\nhistory.", + "description": "Fetches historical turns for a chat. Used for lazy loading of conversation\nhistory.", "properties": { "channel": { "$ref": "#/$defs/URI", - "description": "Session URI" + "description": "Chat URI" }, "before": { "type": "string", @@ -953,7 +953,7 @@ "properties": { "channel": { "$ref": "#/$defs/URI", - "description": "The session URI the completion is being requested for." + "description": "The chat URI the completion is being requested for." }, "kind": { "$ref": "#/$defs/CompletionItemKind", @@ -1017,6 +1017,71 @@ "items" ] }, + "ChatForkSource": { + "type": "object", + "description": "Identifies a source chat and turn to fork from.", + "properties": { + "chat": { + "$ref": "#/$defs/URI", + "description": "URI of the existing chat to fork from" + }, + "turnId": { + "type": "string", + "description": "Turn ID in the source chat; content up to and including this turn's response is copied" + } + }, + "required": [ + "chat", + "turnId" + ] + }, + "CreateChatParams": { + "type": "object", + "description": "Creates a new chat within a session.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Session URI containing the new chat." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "Chat URI (client-chosen, e.g. `ahp-chat:/`)." + }, + "initialMessage": { + "$ref": "#/$defs/Message", + "description": "Optional initial message for the new chat." + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override." + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override." + }, + "source": { + "$ref": "#/$defs/ChatForkSource", + "description": "Optional source chat and turn to fork from." + } + }, + "required": [ + "channel", + "chat" + ] + }, + "DisposeChatParams": { + "type": "object", + "description": "Disposes a chat and cleans up server-side resources.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + } + }, + "required": [ + "channel" + ] + }, "CreateTerminalParams": { "type": "object", "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", @@ -1567,7 +1632,7 @@ "properties": { "resource": { "$ref": "#/$defs/URI", - "description": "The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`)" + "description": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" }, "state": { "oneOf": [ @@ -1765,24 +1830,6 @@ "values" ] }, - "PendingMessage": { - "type": "object", - "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this pending message" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that will start the next turn" - } - }, - "required": [ - "id", - "message" - ] - }, "SessionState": { "type": "object", "description": "Full state for a single session, loaded when a client subscribes to the session's URI.", @@ -1810,34 +1857,16 @@ "$ref": "#/$defs/SessionActiveClient", "description": "The client currently providing tools and interactive capabilities to this session" }, - "turns": { - "type": "array", - "items": { - "$ref": "#/$defs/Turn" - }, - "description": "Completed turns" - }, - "activeTurn": { - "$ref": "#/$defs/ActiveTurn", - "description": "Currently in-progress turn" - }, - "steeringMessage": { - "$ref": "#/$defs/PendingMessage", - "description": "Message to inject into the current turn at a convenient point" - }, - "queuedMessages": { + "chats": { "type": "array", "items": { - "$ref": "#/$defs/PendingMessage" + "$ref": "#/$defs/ChatSummary" }, - "description": "Messages to send automatically as new turns after the current turn finishes" + "description": "Catalog of chats in this session." }, - "inputRequests": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputRequest" - }, - "description": "Requests for user input that are currently blocking or informing session progress" + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "The chat that receives input when the user addresses the session without\nselecting a specific chat. This is a UI routing hint, not a hierarchy\nmarker — chats remain equal peers at the protocol level. Hosts MAY change\nthis over the session's lifetime." }, "config": { "$ref": "#/$defs/SessionConfigState", @@ -1866,7 +1895,7 @@ "required": [ "summary", "lifecycle", - "turns" + "chats" ] }, "SessionActiveClient": { @@ -1921,6 +1950,7 @@ }, "SessionSummary": { "type": "object", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `model` / `agent`: the session-level selection. Per-chat overrides are\n surfaced on individual {@link ChatSummary} entries, not aggregated up.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "resource": { "$ref": "#/$defs/URI", @@ -1964,7 +1994,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory URI for this session" + "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, "changes": { "$ref": "#/$defs/ChangesSummary", @@ -2148,1571 +2178,1679 @@ "values" ] }, - "SessionInputOption": { + "ToolDefinition": { "type": "object", - "description": "A choice in a select-style question.", + "description": "Describes a tool available in a session, provided by either the server or the active client.", "properties": { - "id": { + "name": { "type": "string", - "description": "Stable option identifier; for MCP enum values this is the enum string" + "description": "Unique tool identifier" }, - "label": { + "title": { "type": "string", - "description": "Display label" + "description": "Human-readable display name" }, "description": { "type": "string", - "description": "Optional secondary text" + "description": "Description of what the tool does" }, - "recommended": { - "type": "boolean", - "description": "Whether this option is the recommended/default choice" + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + }, + "outputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + }, + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Behavioral hints about the tool. All properties are advisory." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "id", - "label" + "name" ] }, - "SessionInputQuestionBase": { + "ToolAnnotations": { "type": "object", + "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, "title": { "type": "string", - "description": "Short display title" + "description": "Alternate human-readable title" }, - "message": { - "type": "string", - "description": "Prompt shown to the user" + "readOnlyHint": { + "type": "boolean", + "description": "Tool does not modify its environment (default: false)" }, - "required": { + "destructiveHint": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Tool may perform destructive updates (default: true)" + }, + "idempotentHint": { + "type": "boolean", + "description": "Repeated calls with the same arguments have no additional effect (default: false)" + }, + "openWorldHint": { + "type": "boolean", + "description": "Tool may interact with external entities (default: true)" } - }, - "required": [ - "id", - "message" - ] + } }, - "SessionInputTextQuestion": { + "CustomizationBase": { "type": "object", - "description": "Text question within a session input request.", + "description": "Fields shared by every customization variant.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Text" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "format": { + "name": { "type": "string", - "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" - }, - "min": { - "type": "number", - "description": "Minimum string length" + "description": "Human-readable name." }, - "max": { - "type": "number", - "description": "Maximum string length" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "defaultValue": { - "type": "string", - "description": "Default text" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." } }, "required": [ "id", - "message", - "kind" + "uri", + "name" ] }, - "SessionInputNumberQuestion": { + "CustomizationLoadingState": { "type": "object", - "description": "Numeric question within a session input request.", + "description": "Container is being loaded by the host.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputQuestionKind.Number" - }, - { - "$ref": "#/$defs/SessionInputQuestionKind.Integer" - } - ] - }, - "min": { - "type": "number", - "description": "Minimum value" - }, - "max": { - "type": "number", - "description": "Maximum value" - }, - "defaultValue": { - "type": "number", - "description": "Default numeric value" + "$ref": "#/$defs/CustomizationLoadStatus.Loading" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputBooleanQuestion": { + "CustomizationLoadedState": { "type": "object", - "description": "Boolean question within a session input request.", + "description": "Container loaded successfully.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" + "kind": { + "$ref": "#/$defs/CustomizationLoadStatus.Loaded" + } + }, + "required": [ + "kind" + ] + }, + "CustomizationDegradedState": { + "type": "object", + "description": "Container partially loaded but has warnings.", + "properties": { + "kind": { + "$ref": "#/$defs/CustomizationLoadStatus.Degraded" }, "message": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, + "description": "Human-readable description of the warning." + } + }, + "required": [ + "kind", + "message" + ] + }, + "CustomizationErrorState": { + "type": "object", + "description": "Container failed to load.", + "properties": { "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Boolean" + "$ref": "#/$defs/CustomizationLoadStatus.Error" }, - "defaultValue": { - "type": "boolean", - "description": "Default boolean value" + "message": { + "type": "string", + "description": "Human-readable error message." } }, "required": [ - "id", - "message", - "kind" + "kind", + "message" ] }, - "SessionInputSingleSelectQuestion": { + "ContainerCustomizationBase": { "type": "object", - "description": "Single-select question within a session input request.", + "description": "Fields shared by container customizations.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "title": { - "type": "string", - "description": "Short display title" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "message": { + "name": { "type": "string", - "description": "Prompt shown to the user" + "description": "Human-readable name." }, - "required": { + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Whether this container is currently enabled." }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.SingleSelect" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "options": { + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputOption" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Options the user may select from" - }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text instead of selecting an option" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." } }, "required": [ "id", - "message", - "kind", - "options" + "uri", + "name", + "enabled" ] }, - "SessionInputMultiSelectQuestion": { + "PluginCustomization": { "type": "object", - "description": "Multi-select question within a session input request.", + "description": "An [Open Plugins](https://open-plugins.com/) plugin.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "title": { - "type": "string", - "description": "Short display title" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "message": { + "name": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.MultiSelect" + "description": "Human-readable name." }, - "options": { + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputOption" + "$ref": "#/$defs/Icon" }, - "description": "Options the user may select from" + "description": "Icons for UI display." }, - "allowFreeformInput": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { "type": "boolean", - "description": "Whether the user may enter text in addition to selecting options" + "description": "Whether this container is currently enabled." }, - "min": { - "type": "number", - "description": "Minimum selected item count" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "max": { - "type": "number", - "description": "Maximum selected item count" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" } }, "required": [ "id", - "message", - "kind", - "options" + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputRequest": { + "ClientPluginCustomization": { "type": "object", - "description": "A live request for user input.\n\nThe server creates or replaces requests with `session/inputRequested`.\nClients sync drafts with `session/inputAnswerChanged` and complete requests\nwith `session/inputCompleted`.", + "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", "properties": { "id": { "type": "string", - "description": "Stable request identifier" - }, - "message": { - "type": "string", - "description": "Display message for the request as a whole" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "url": { + "uri": { "$ref": "#/$defs/URI", - "description": "URL the user should review or open, for URL-style elicitations" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "questions": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputQuestion" + "$ref": "#/$defs/Icon" }, - "description": "Ordered questions to ask the user" + "description": "Icons for UI display." }, - "answers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/SessionInputAnswer" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" }, - "description": "Current draft or submitted answers, keyed by question ID" - } - }, - "required": [ - "id" - ] - }, - "SessionInputTextAnswerValue": { - "type": "object", - "description": "Value captured for one answer.", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Text" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." }, - "value": { - "type": "string" + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" + }, + "nonce": { + "type": "string", + "description": "Opaque version token used by the host to detect changes." } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputNumberAnswerValue": { + "DirectoryCustomization": { "type": "object", + "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Number" - }, - "value": { - "type": "number" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputBooleanAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Boolean" + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "value": { - "type": "boolean" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputSelectedAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Selected" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "value": { - "type": "string" + "name": { + "type": "string", + "description": "Human-readable name." }, - "freeformValues": { + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "description": "Free-form text entered instead of selecting an option" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputSelectedManyAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.SelectedMany" + "description": "Icons for UI display." }, - "value": { - "type": "array", - "items": { - "type": "string" - } + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "freeformValues": { + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered in addition to selected options" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputAnswered": { - "type": "object", - "properties": { - "state": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputAnswerState.Draft" - }, - { - "$ref": "#/$defs/SessionInputAnswerState.Submitted" - } - ], - "description": "Answer state" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." }, - "value": { - "$ref": "#/$defs/SessionInputAnswerValue", - "description": "Answer value" + "type": { + "$ref": "#/$defs/CustomizationType.Directory" + }, + "contents": { + "$ref": "#/$defs/ChildCustomizationType", + "description": "Which child customization type this directory holds." + }, + "writable": { + "type": "boolean", + "description": "Whether clients may write into this directory." } }, "required": [ - "state", - "value" + "id", + "uri", + "name", + "enabled", + "type", + "contents", + "writable" ] }, - "SessionInputSkipped": { + "AgentCustomization": { "type": "object", + "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", "properties": { - "state": { - "$ref": "#/$defs/SessionInputAnswerState.Skipped", - "description": "Answer state" + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "freeformValues": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "description": "Free-form reason or value captured while skipping, if any" + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Agent" + }, + "description": { + "type": "string", + "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "state" + "id", + "uri", + "name", + "type" ] }, - "Turn": { + "SkillCustomization": { "type": "object", - "description": "A completed request/response cycle.", + "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "responseParts": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/Icon" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." + "description": "Icons for UI display." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "state": { - "$ref": "#/$defs/TurnState", - "description": "How the turn ended" + "type": { + "$ref": "#/$defs/CustomizationType.Skill" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details if state is `'error'`" + "description": { + "type": "string", + "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." } }, "required": [ "id", - "message", - "responseParts", - "usage", - "state" + "uri", + "name", + "type" ] }, - "ActiveTurn": { + "PromptCustomization": { "type": "object", - "description": "An in-progress turn — the assistant is actively streaming.", + "description": "A prompt contributed by a plugin or directory.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "responseParts": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/Icon" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + "description": "Icons for UI display." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Prompt" + }, + "description": { + "type": "string", + "description": "Short description of what the prompt does." } }, "required": [ "id", - "message", - "responseParts", - "usage" + "uri", + "name", + "type" ] }, - "Message": { + "RuleCustomization": { "type": "object", - "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", "properties": { - "text": { + "id": { "type": "string", - "description": "Message text" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "origin": { - "type": "object", - "properties": { - "kind": { - "type": "string" - } - }, - "required": [ - "kind" - ], - "description": "The origin of the message" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "attachments": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/MessageAttachment" + "$ref": "#/$defs/Icon" }, - "description": "File/selection attachments" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." - } - }, - "required": [ - "text", - "origin" - ] - }, - "MessageAttachmentBase": { - "type": "object", - "description": "Common fields shared by all {@link MessageAttachment} variants.", - "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Icons for UI display." }, "range": { "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "displayKind": { + "type": { + "$ref": "#/$defs/CustomizationType.Rule" + }, + "description": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Description of what the rule enforces." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "alwaysApply": { + "type": "boolean", + "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." + }, + "globs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." } }, "required": [ - "label" + "id", + "uri", + "name", + "type" ] }, - "SimpleMessageAttachment": { + "HookCustomization": { "type": "object", - "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", + "description": "A hook manifest contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Simple", - "description": "Discriminant" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "modelRepresentation": { - "type": "string", - "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + "type": { + "$ref": "#/$defs/CustomizationType.Hook" } }, "required": [ - "label", + "id", + "uri", + "name", "type" ] }, - "MessageEmbeddedResourceAttachment": { + "McpServerCustomization": { "type": "object", - "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "type": { - "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", - "description": "Discriminant" + "$ref": "#/$defs/CustomizationType.McpServer" }, - "data": { - "type": "string", - "description": "Base64-encoded binary data" + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." }, - "contentType": { - "type": "string", - "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ - "label", + "id", + "uri", + "name", "type", - "data", - "contentType" + "enabled", + "state" ] }, - "MessageResourceAttachment": { + "McpServerCustomizationApps": { "type": "object", - "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." - }, - "displayKind": { - "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" - }, - "contentType": { - "type": "string", - "description": "Content MIME type" - }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Resource", - "description": "Discriminant" - }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." } }, "required": [ - "label", - "uri", - "type" + "capabilities" ] }, - "MessageAnnotationsAttachment": { + "AhpMcpUiHostCapabilities": { "type": "object", - "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." - }, - "displayKind": { - "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "_meta": { + "serverTools": { "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." - }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Annotations", - "description": "Discriminant" + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." }, - "resource": { - "$ref": "#/$defs/URI", - "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." }, - "annotationIds": { - "type": "array", - "items": { - "type": "string" + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } }, - "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." } - }, - "required": [ - "label", - "type", - "resource" - ] + } }, - "MarkdownResponsePart": { + "McpServerStartingState": { "type": "object", + "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.Markdown", - "description": "Discriminant" - }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/delta` to target this part for content appends" - }, - "content": { - "type": "string", - "description": "Markdown content" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ - "kind", - "id", - "content" + "kind" ] }, - "ResourceReponsePart": { + "McpServerReadyState": { "type": "object", - "description": "A content part that's a reference to large content stored outside the state tree.", + "description": "Server is running and serving requests.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" - }, - "contentType": { - "type": "string", - "description": "Content MIME type" - }, "kind": { - "$ref": "#/$defs/ResponsePartKind.ContentRef", - "description": "Discriminant" + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ - "uri", "kind" ] }, - "ToolCallResponsePart": { + "McpServerAuthRequiredState": { "type": "object", - "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.ToolCall", - "description": "Discriminant" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "Full tool call lifecycle state" + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." } }, "required": [ "kind", - "toolCall" + "reason", + "resource" ] }, - "ReasoningResponsePart": { + "McpServerErrorState": { "type": "object", - "description": "Reasoning/thinking content from the model.", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.Reasoning", - "description": "Discriminant" - }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/reasoning` to target this part for content appends" + "$ref": "#/$defs/McpServerStatus.Error" }, - "content": { - "type": "string", - "description": "Accumulated reasoning text" + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." } }, "required": [ "kind", - "id", - "content" + "error" ] }, - "SystemNotificationResponsePart": { + "McpServerStoppedState": { "type": "object", - "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.SystemNotification", - "description": "Discriminant" - }, - "content": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "The text of the system notification" + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ - "kind", - "content" + "kind" ] }, - "ConfirmationOption": { + "ChatState": { "type": "object", - "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", "properties": { - "id": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" + }, + "title": { "type": "string", - "description": "Unique identifier for the option, returned in the confirmed action" + "description": "Chat title" }, - "label": { + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { "type": "string", - "description": "Human-readable label displayed to the user" + "description": "Human-readable description of what the chat is currently doing" }, - "kind": { - "$ref": "#/$defs/ConfirmationOptionKind", - "description": "Whether this option represents an approval or denial" + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "group": { - "type": "number", - "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + }, + "turns": { + "type": "array", + "items": { + "$ref": "#/$defs/Turn" + }, + "description": "Completed turns" + }, + "activeTurn": { + "$ref": "#/$defs/ActiveTurn", + "description": "Currently in-progress turn" + }, + "steeringMessage": { + "$ref": "#/$defs/PendingMessage", + "description": "Message to inject into the current turn at a convenient point" + }, + "queuedMessages": { + "type": "array", + "items": { + "$ref": "#/$defs/PendingMessage" + }, + "description": "Messages to send automatically as new turns after the current turn finishes" + }, + "inputRequests": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputRequest" + }, + "description": "Requests for user input that are currently blocking or informing chat progress" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this chat." } }, "required": [ - "id", - "label", - "kind" + "resource", + "title", + "status", + "modifiedAt", + "turns" ] }, - "ToolCallClientContributor": { + "ChatSummary": { "type": "object", + "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallContributorKind.Client" + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" }, - "clientId": { + "title": { + "type": "string", + "description": "Chat title" + }, + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { + "type": "string", + "description": "Human-readable description of what the chat is currently doing" + }, + "modifiedAt": { "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." } }, "required": [ - "kind", - "clientId" + "resource", + "title", + "status", + "modifiedAt" ] }, - "ToolCallMcpContributor": { + "PendingMessage": { "type": "object", + "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallContributorKind.MCP" - }, - "customizationId": { + "id": { "type": "string", - "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + "description": "Unique identifier for this pending message" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "The message that will start the next turn" } }, "required": [ - "kind", - "customizationId" + "id", + "message" ] }, - "ToolCallBase": { + "ChatInputOption": { "type": "object", - "description": "Metadata common to all tool call states.", + "description": "A choice in a select-style question.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable option identifier; for MCP enum values this is the enum string" }, - "toolName": { + "label": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Display label" }, - "displayName": { + "description": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "description": "Optional secondary text" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "recommended": { + "type": "boolean", + "description": "Whether this option is the recommended/default choice" } }, "required": [ - "toolCallId", - "toolName", - "displayName" + "id", + "label" ] }, - "ToolCallParameterFields": { + "ChatInputQuestionBase": { "type": "object", - "description": "Properties available once tool call parameters are fully received.", "properties": { - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "toolInput": { + "title": { "type": "string", - "description": "Raw tool input" + "description": "Short display title" + }, + "message": { + "type": "string", + "description": "Prompt shown to the user" + }, + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" } }, "required": [ - "invocationMessage" + "id", + "message" ] }, - "ToolCallResult": { + "ChatInputTextQuestion": { "type": "object", - "description": "Tool execution result details, available after execution completes.", + "description": "Text question within a chat input request.", "properties": { - "success": { + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" + }, + "title": { + "type": "string", + "description": "Short display title" + }, + "message": { + "type": "string", + "description": "Prompt shown to the user" + }, + "required": { "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Whether the user must answer this question to accept the request" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Text" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "format": { + "type": "string", + "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "min": { + "type": "number", + "description": "Minimum string length" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "max": { + "type": "number", + "description": "Maximum string length" + }, + "defaultValue": { + "type": "string", + "description": "Default text" } }, "required": [ - "success", - "pastTenseMessage" + "id", + "message", + "kind" ] }, - "ToolCallStreamingState": { + "ChatInputNumberQuestion": { "type": "object", - "description": "LM is streaming the tool call parameters.", + "description": "Numeric question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" + "description": "Prompt shown to the user" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputQuestionKind.Number" + }, + { + "$ref": "#/$defs/ChatInputQuestionKind.Integer" + } + ] }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Streaming" + "min": { + "type": "number", + "description": "Minimum value" }, - "partialInput": { - "type": "string", - "description": "Partial parameters accumulated so far" + "max": { + "type": "number", + "description": "Maximum value" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Progress message shown while parameters are streaming" + "defaultValue": { + "type": "number", + "description": "Default numeric value" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "status" + "id", + "message", + "kind" ] }, - "ToolCallPendingConfirmationState": { + "ChatInputBooleanQuestion": { "type": "object", - "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "description": "Boolean question within a chat input request.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { + "id": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Stable question identifier used as the key in `answers`" }, - "displayName": { + "title": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Short display title" }, - "toolInput": { + "message": { "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + "description": "Prompt shown to the user" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "string" - } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Boolean" }, - "editable": { + "defaultValue": { "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ConfirmationOption" - }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "description": "Default boolean value" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status" + "id", + "message", + "kind" ] }, - "ToolCallRunningState": { + "ChatInputSingleSelectQuestion": { "type": "object", - "description": "Tool is actively executing.", + "description": "Single-select question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" - }, - "toolInput": { - "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Running" + "description": "Prompt shown to the user" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.SingleSelect" }, - "content": { + "options": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChatInputOption" }, - "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + "description": "Options the user may select from" + }, + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text instead of selecting an option" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "confirmed" + "id", + "message", + "kind", + "options" ] }, - "ToolCallPendingResultConfirmationState": { + "ChatInputMultiSelectQuestion": { "type": "object", - "description": "Tool finished executing, waiting for client to approve the result.", + "description": "Multi-select question within a chat input request.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { + "id": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Stable question identifier used as the key in `answers`" }, - "displayName": { + "title": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Short display title" }, - "toolInput": { + "message": { "type": "string", - "description": "Raw tool input" + "description": "Prompt shown to the user" }, - "success": { + "required": { "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Whether the user must answer this question to accept the request" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.MultiSelect" }, - "content": { + "options": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "$ref": "#/$defs/ChatInputOption" }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "description": "Options the user may select from" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text in addition to selecting options" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "min": { + "type": "number", + "description": "Minimum selected item count" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "max": { + "type": "number", + "description": "Maximum selected item count" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id", + "message", + "kind", + "options" ] }, - "ToolCallCompletedState": { + "ChatInputRequest": { "type": "object", - "description": "Tool completed successfully or with an error.", + "description": "A live request for user input.\n\nThe server creates or replaces requests with `chat/inputRequested`.\nClients sync drafts with `chat/inputAnswerChanged` and complete requests\nwith `chat/inputCompleted`.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { + "id": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Stable request identifier" }, - "toolInput": { + "message": { "type": "string", - "description": "Raw tool input" - }, - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Display message for the request as a whole" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "url": { + "$ref": "#/$defs/URI", + "description": "URL the user should review or open, for URL-style elicitations" }, - "content": { + "questions": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChatInputQuestion" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "description": "Ordered questions to ask the user" }, - "error": { + "answers": { "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "additionalProperties": { + "$ref": "#/$defs/ChatInputAnswer" }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Completed" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" - }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "description": "Current draft or submitted answers, keyed by question ID" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id" ] }, - "ToolCallCancelledState": { + "ChatInputTextAnswerValue": { "type": "object", - "description": "Tool call was cancelled before execution.", + "description": "Value captured for one answer.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Text" }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputNumberAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Number" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "value": { + "type": "number" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputBooleanAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Boolean" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "value": { + "type": "boolean" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputSelectedAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Selected" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "value": { + "type": "string" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered instead of selecting an option" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputSelectedManyAnswerValue": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.SelectedMany" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Cancelled" + "value": { + "type": "array", + "items": { + "type": "string" + } }, - "reason": { - "$ref": "#/$defs/ToolCallCancellationReason", - "description": "Why the tool was cancelled" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered in addition to selected options" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputAnswered": { + "type": "object", + "properties": { + "state": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputAnswerState.Draft" + }, + { + "$ref": "#/$defs/ChatInputAnswerState.Submitted" + } + ], + "description": "Answer state" }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional message explaining the cancellation" + "value": { + "$ref": "#/$defs/ChatInputAnswerValue", + "description": "Answer value" + } + }, + "required": [ + "state", + "value" + ] + }, + "ChatInputSkipped": { + "type": "object", + "properties": { + "state": { + "$ref": "#/$defs/ChatInputAnswerState.Skipped", + "description": "Answer state" }, - "userSuggestion": { + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form reason or value captured while skipping, if any" + } + }, + "required": [ + "state" + ] + }, + "Turn": { + "type": "object", + "description": "A completed request/response cycle.", + "properties": { + "id": { + "type": "string", + "description": "Turn identifier" + }, + "message": { "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" + "description": "The message that initiated the turn" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" + }, + "state": { + "$ref": "#/$defs/TurnState", + "description": "How the turn ended" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details if state is `'error'`" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "reason" + "id", + "message", + "responseParts", + "usage", + "state" ] }, - "ToolDefinition": { + "ActiveTurn": { "type": "object", - "description": "Describes a tool available in a session, provided by either the server or the active client.", + "description": "An in-progress turn — the assistant is actively streaming.", "properties": { - "name": { + "id": { "type": "string", - "description": "Unique tool identifier" + "description": "Turn identifier" }, - "title": { - "type": "string", - "description": "Human-readable display name" + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "description": { + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" + } + }, + "required": [ + "id", + "message", + "responseParts", + "usage" + ] + }, + "Message": { + "type": "object", + "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "properties": { + "text": { "type": "string", - "description": "Description of what the tool does" + "description": "Message text" }, - "inputSchema": { + "origin": { "type": "object", "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { + "kind": { "type": "string" } }, "required": [ - "type" + "kind" ], - "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + "description": "The origin of the message" }, - "outputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } + "attachments": { + "type": "array", + "items": { + "$ref": "#/$defs/MessageAttachment" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." - }, - "annotations": { - "$ref": "#/$defs/ToolAnnotations", - "description": "Behavioral hints about the tool. All properties are advisory." + "description": "File/selection attachments" }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." + "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." } }, "required": [ - "name" + "text", + "origin" ] }, - "ToolAnnotations": { + "MessageAttachmentBase": { "type": "object", - "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", + "description": "Common fields shared by all {@link MessageAttachment} variants.", "properties": { - "title": { + "label": { "type": "string", - "description": "Alternate human-readable title" - }, - "readOnlyHint": { - "type": "boolean", - "description": "Tool does not modify its environment (default: false)" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "destructiveHint": { - "type": "boolean", - "description": "Tool may perform destructive updates (default: true)" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "idempotentHint": { - "type": "boolean", - "description": "Repeated calls with the same arguments have no additional effect (default: false)" + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "openWorldHint": { - "type": "boolean", - "description": "Tool may interact with external entities (default: true)" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." } - } + }, + "required": [ + "label" + ] }, - "ToolResultTextContent": { + "SimpleMessageAttachment": { "type": "object", - "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", + "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, "type": { - "$ref": "#/$defs/ToolResultContentType.Text" + "$ref": "#/$defs/MessageAttachmentKind.Simple", + "description": "Discriminant" }, - "text": { + "modelRepresentation": { "type": "string", - "description": "The text content" + "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." } }, "required": [ - "type", - "text" + "label", + "type" ] }, - "ToolResultEmbeddedResourceContent": { + "MessageEmbeddedResourceAttachment": { "type": "object", - "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", + "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, "type": { - "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" + "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", + "description": "Discriminant" }, "data": { "type": "string", - "description": "Base64-encoded data" + "description": "Base64-encoded binary data" }, "contentType": { "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ + "label", "type", "data", "contentType" ] }, - "ToolResultResourceContent": { + "MessageResourceAttachment": { "type": "object", - "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", + "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, "uri": { "$ref": "#/$defs/URI", "description": "Content URI" @@ -3726,873 +3864,914 @@ "description": "Content MIME type" }, "type": { - "$ref": "#/$defs/ToolResultContentType.Resource" + "$ref": "#/$defs/MessageAttachmentKind.Resource", + "description": "Discriminant" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ + "label", "uri", "type" ] }, - "ToolResultFileEditContent": { + "MessageAnnotationsAttachment": { "type": "object", - "description": "Describes a file modification performed by a tool.", + "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", "properties": { - "before": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "after": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "diff": { + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } - }, - "description": "Optional diff display metadata" + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, "type": { - "$ref": "#/$defs/ToolResultContentType.FileEdit" + "$ref": "#/$defs/MessageAttachmentKind.Annotations", + "description": "Discriminant" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + }, + "annotationIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." } }, "required": [ - "type" + "label", + "type", + "resource" ] }, - "ToolResultTerminalContent": { + "MarkdownResponsePart": { "type": "object", - "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Terminal" + "kind": { + "$ref": "#/$defs/ResponsePartKind.Markdown", + "description": "Discriminant" }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "id": { + "type": "string", + "description": "Part identifier, used by `chat/delta` to target this part for content appends" }, - "title": { + "content": { "type": "string", - "description": "Display title for the terminal content" + "description": "Markdown content" } }, "required": [ - "type", - "resource", - "title" + "kind", + "id", + "content" ] }, - "ToolResultSubagentContent": { + "ResourceReponsePart": { "type": "object", - "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", + "description": "A content part that's a reference to large content stored outside the state tree.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Subagent" - }, - "resource": { + "uri": { "$ref": "#/$defs/URI", - "description": "Subagent session URI (subscribable for full session state)" + "description": "Content URI" }, - "title": { - "type": "string", - "description": "Display title for the subagent" + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "agentName": { + "contentType": { "type": "string", - "description": "Internal agent name" + "description": "Content MIME type" + }, + "kind": { + "$ref": "#/$defs/ResponsePartKind.ContentRef", + "description": "Discriminant" + } + }, + "required": [ + "uri", + "kind" + ] + }, + "ToolCallResponsePart": { + "type": "object", + "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", + "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.ToolCall", + "description": "Discriminant" }, - "description": { - "type": "string", - "description": "Human-readable description of the subagent's task" + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "Full tool call lifecycle state" } }, "required": [ - "type", - "resource", - "title" + "kind", + "toolCall" ] }, - "CustomizationBase": { + "ReasoningResponsePart": { "type": "object", - "description": "Fields shared by every customization variant.", + "description": "Reasoning/thinking content from the model.", "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.Reasoning", + "description": "Discriminant" + }, "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { + "content": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "Accumulated reasoning text" } }, "required": [ + "kind", "id", - "uri", - "name" + "content" ] }, - "CustomizationLoadingState": { + "SystemNotificationResponsePart": { "type": "object", - "description": "Container is being loaded by the host.", + "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", "properties": { "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loading" + "$ref": "#/$defs/ResponsePartKind.SystemNotification", + "description": "Discriminant" + }, + "content": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "The text of the system notification" } }, "required": [ - "kind" + "kind", + "content" ] }, - "CustomizationLoadedState": { + "ConfirmationOption": { "type": "object", - "description": "Container loaded successfully.", + "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the option, returned in the confirmed action" + }, + "label": { + "type": "string", + "description": "Human-readable label displayed to the user" + }, "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loaded" + "$ref": "#/$defs/ConfirmationOptionKind", + "description": "Whether this option represents an approval or denial" + }, + "group": { + "type": "number", + "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." } }, "required": [ + "id", + "label", "kind" ] }, - "CustomizationDegradedState": { + "ToolCallClientContributor": { "type": "object", - "description": "Container partially loaded but has warnings.", "properties": { "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Degraded" + "$ref": "#/$defs/ToolCallContributorKind.Client" }, - "message": { + "clientId": { "type": "string", - "description": "Human-readable description of the warning." + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." } }, "required": [ "kind", - "message" + "clientId" ] }, - "CustomizationErrorState": { + "ToolCallMcpContributor": { "type": "object", - "description": "Container failed to load.", "properties": { "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Error" + "$ref": "#/$defs/ToolCallContributorKind.MCP" }, - "message": { + "customizationId": { "type": "string", - "description": "Human-readable error message." + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." } }, "required": [ "kind", - "message" + "customizationId" ] }, - "ContainerCustomizationBase": { + "ToolCallBase": { "type": "object", - "description": "Fields shared by container customizations.", + "description": "Metadata common to all tool call states.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Unique tool call identifier" }, - "name": { + "toolName": { "type": "string", - "description": "Human-readable name." + "description": "Internal tool name (for debugging/logging)" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "displayName": { + "type": "string", + "description": "Human-readable tool name" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName" + ] + }, + "ToolCallParameterFields": { + "type": "object", + "description": "Properties available once tool call parameters are fully received.", + "properties": { + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "clientId": { + "toolInput": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Raw tool input" + } + }, + "required": [ + "invocationMessage" + ] + }, + "ToolCallResult": { + "type": "object", + "description": "Tool execution result details, available after execution completes.", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" }, - "children": { + "content": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" } }, "required": [ - "id", - "uri", - "name", - "enabled" + "success", + "pastTenseMessage" ] }, - "PluginCustomization": { + "ToolCallStreamingState": { "type": "object", - "description": "An [Open Plugins](https://open-plugins.com/) plugin.", + "description": "LM is streaming the tool call parameters.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Unique tool call identifier" }, - "name": { + "toolName": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "Internal tool name (for debugging/logging)" }, - "clientId": { + "displayName": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Human-readable tool name" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "status": { + "$ref": "#/$defs/ToolCallStatus.Streaming" }, - "type": { - "$ref": "#/$defs/CustomizationType.Plugin" + "partialInput": { + "type": "string", + "description": "Partial parameters accumulated so far" + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Progress message shown while parameters are streaming" } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "toolCallId", + "toolName", + "displayName", + "status" ] }, - "ClientPluginCustomization": { + "ToolCallPendingConfirmationState": { "type": "object", - "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", + "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "clientId": { + "toolInput": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Raw tool input" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "edits": { + "type": "object", + "properties": { + "items": { + "type": "string" + } }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" }, - "type": { - "$ref": "#/$defs/CustomizationType.Plugin" + "editable": { + "type": "boolean", + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" }, - "nonce": { - "type": "string", - "description": "Opaque version token used by the host to detect changes." + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfirmationOption" + }, + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status" ] }, - "DirectoryCustomization": { + "ToolCallRunningState": { "type": "object", - "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", + "description": "Tool is actively executing.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "clientId": { + "toolInput": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Raw tool input" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "status": { + "$ref": "#/$defs/ToolCallStatus.Running" }, - "children": { + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + }, + "content": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Directory" - }, - "contents": { - "$ref": "#/$defs/ChildCustomizationType", - "description": "Which child customization type this directory holds." - }, - "writable": { - "type": "boolean", - "description": "Whether clients may write into this directory." + "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type", - "contents", - "writable" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "confirmed" ] }, - "AgentCustomization": { + "ToolCallPendingResultConfirmationState": { "type": "object", - "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", + "description": "Tool finished executing, waiting for client to approve the result.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Icons for UI display." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "type": { - "$ref": "#/$defs/CustomizationType.Agent" + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" }, - "description": { - "type": "string", - "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "SkillCustomization": { + "ToolCallCompletedState": { "type": "object", - "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", + "description": "Tool completed successfully or with an error.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "$ref": "#/$defs/CustomizationType.Skill" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "description": { + "toolInput": { "type": "string", - "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + "description": "Raw tool input" }, - "disableModelInvocation": { + "success": { "type": "boolean", - "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." - } - }, - "required": [ - "id", - "uri", - "name", - "type" - ] - }, - "PromptCustomization": { - "type": "object", - "description": "A prompt contributed by a plugin or directory.", - "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Whether the tool succeeded" }, - "name": { - "type": "string", - "description": "Human-readable name." + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" }, - "icons": { + "content": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Icons for UI display." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "type": { - "$ref": "#/$defs/CustomizationType.Prompt" + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "description": { - "type": "string", - "description": "Short description of what the prompt does." + "status": { + "$ref": "#/$defs/ToolCallStatus.Completed" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "RuleCustomization": { + "ToolCallCancelledState": { "type": "object", - "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", + "description": "Tool call was cancelled before execution.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "$ref": "#/$defs/CustomizationType.Rule" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "description": { + "toolInput": { "type": "string", - "description": "Description of what the rule enforces." + "description": "Raw tool input" }, - "alwaysApply": { - "type": "boolean", - "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." + "status": { + "$ref": "#/$defs/ToolCallStatus.Cancelled" }, - "globs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." + "reason": { + "$ref": "#/$defs/ToolCallCancellationReason", + "description": "Why the tool was cancelled" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional message explaining the cancellation" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "reason" ] }, - "HookCustomization": { + "ToolResultTextContent": { "type": "object", - "description": "A hook manifest contributed by a plugin or directory.", + "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "type": { + "$ref": "#/$defs/ToolResultContentType.Text" }, - "name": { + "text": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Hook" + "description": "The text content" } }, "required": [ - "id", - "uri", - "name", - "type" + "type", + "text" ] }, - "McpServerCustomization": { + "ToolResultEmbeddedResourceContent": { "type": "object", - "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", + "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, "type": { - "$ref": "#/$defs/CustomizationType.McpServer" - }, - "enabled": { - "type": "boolean", - "description": "Whether this MCP server is currently enabled." - }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "Current lifecycle state of the MCP server." + "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" }, - "channel": { - "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "data": { + "type": "string", + "description": "Base64-encoded data" }, - "mcpApp": { - "$ref": "#/$defs/McpServerCustomizationApps", - "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" } }, "required": [ - "id", - "uri", - "name", "type", - "enabled", - "state" + "data", + "contentType" ] }, - "McpServerCustomizationApps": { + "ToolResultResourceContent": { "type": "object", - "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "capabilities": { - "$ref": "#/$defs/AhpMcpUiHostCapabilities", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.Resource" } }, "required": [ - "capabilities" + "uri", + "type" ] }, - "AhpMcpUiHostCapabilities": { + "ToolResultFileEditContent": { "type": "object", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "description": "Describes a file modification performed by a tool.", "properties": { - "serverTools": { + "before": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." }, - "serverResources": { + "after": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `resources/*` methods to the upstream server." - }, - "logging": { - "type": "object", - "additionalProperties": {}, - "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." }, - "sampling": { + "diff": { "type": "object", "properties": { - "tools": { - "type": "string" + "added": { + "type": "number" + }, + "removed": { + "type": "number" } }, - "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." - } - } - }, - "McpServerStartingState": { - "type": "object", - "description": "Server is registered with the host but has not yet started.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Starting" - } - }, - "required": [ - "kind" - ] - }, - "McpServerReadyState": { - "type": "object", - "description": "Server is running and serving requests.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Ready" + "description": "Optional diff display metadata" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.FileEdit" } }, "required": [ - "kind" + "type" ] }, - "McpServerAuthRequiredState": { + "ToolResultTerminalContent": { "type": "object", - "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.AuthRequired" - }, - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "type": { + "$ref": "#/$defs/ToolResultContentType.Terminal" }, "resource": { - "$ref": "#/$defs/ProtectedResourceMetadata", - "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." - }, - "requiredScopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" }, - "description": { + "title": { "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." + "description": "Display title for the terminal content" } }, "required": [ - "kind", - "reason", - "resource" + "type", + "resource", + "title" ] }, - "McpServerErrorState": { + "ToolResultSubagentContent": { "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", + "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Error" + "type": { + "$ref": "#/$defs/ToolResultContentType.Subagent" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details." - } - }, - "required": [ - "kind", - "error" - ] - }, - "McpServerStoppedState": { - "type": "object", - "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Stopped" + "resource": { + "$ref": "#/$defs/URI", + "description": "Subagent session URI (subscribable for full session state)" + }, + "title": { + "type": "string", + "description": "Display title for the subagent" + }, + "agentName": { + "type": "string", + "description": "Internal agent name" + }, + "description": { + "type": "string", + "description": "Human-readable description of the subagent's task" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, "TerminalInfo": { @@ -5217,29 +5396,6 @@ "config" ] }, - "ToolCallActionBase": { - "type": "object", - "description": "Base interface for all tool-call-scoped actions, carrying the common turn\nand tool call identifiers. The owning session URI is identified by the\nenclosing {@link ActionEnvelope}'s `channel` field.", - "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - } - }, - "required": [ - "turnId", - "toolCallId" - ] - }, "SessionReadyAction": { "type": "object", "description": "Session backend initialized successfully.", @@ -5269,528 +5425,496 @@ "error" ] }, - "SessionTurnStartedAction": { + "SessionChatAddedAction": { "type": "object", - "description": "A new message has been sent to the agent, and a new turn starts.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A chat was added to this session's catalog. Upsert semantics: if a chat\nwith the same `summary.resource` already exists, the existing entry is\nreplaced.\n\nMirrors the root-channel `root/sessionAdded` notification.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTurnStarted" - }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "$ref": "#/$defs/ActionType.SessionChatAdded" }, - "message": { - "$ref": "#/$defs/Message", - "description": "The new message" - }, - "queuedMessageId": { - "type": "string", - "description": "If this turn was auto-started from a queued message, the ID of that message" + "summary": { + "$ref": "#/$defs/ChatSummary", + "description": "The full summary of the newly added (or upserted) chat." } }, "required": [ "type", - "turnId", - "message" + "summary" ] }, - "SessionDeltaAction": { + "SessionChatRemovedAction": { "type": "object", - "description": "Streaming text chunk from the assistant, appended to a specific response part.\n\nThe server MUST first emit a `session/responsePart` to create the target\npart (markdown or reasoning), then use this action to append text to it.", + "description": "A chat was removed from this session's catalog. No-op when no entry matches.\n\nMirrors the root-channel `root/sessionRemoved` notification.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionDelta" - }, - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "partId": { - "type": "string", - "description": "Identifier of the response part to append to" + "$ref": "#/$defs/ActionType.SessionChatRemoved" }, - "content": { - "type": "string", - "description": "Text chunk" + "chat": { + "$ref": "#/$defs/URI", + "description": "The URI of the chat to remove." } }, "required": [ "type", - "turnId", - "partId", - "content" + "chat" ] }, - "SessionResponsePartAction": { + "SessionChatUpdatedAction": { "type": "object", - "description": "Structured content appended to the response.", + "description": "One existing chat's summary fields changed.\n\nPartial-update semantics: only fields present in `changes` are written;\nomitted fields are preserved. Identity fields (`resource`) MUST NOT be\ncarried in `changes`. No-op when no entry with `chat` exists — clients\nSHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}.\n\nMirrors the root-channel `root/sessionSummaryChanged` notification.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionResponsePart" + "$ref": "#/$defs/ActionType.SessionChatUpdated" }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "chat": { + "$ref": "#/$defs/URI", + "description": "The URI of the chat whose summary changed." }, - "part": { - "$ref": "#/$defs/ResponsePart", - "description": "Response part (markdown or content ref)" + "changes": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" + }, + "title": { + "type": "string", + "description": "Chat title" + }, + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { + "type": "string", + "description": "Human-readable description of what the chat is currently doing" + }, + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + } + }, + "description": "Mutable summary fields that changed; omitted fields are unchanged.\n\nIdentity fields (`resource`) never change and MUST be omitted by\nsenders; receivers SHOULD ignore them if present." } }, "required": [ "type", - "turnId", - "part" + "chat", + "changes" ] }, - "SessionToolCallStartAction": { + "SessionDefaultChatChangedAction": { "type": "object", - "description": "A tool call begins — parameters are streaming from the LM.\n\nThe server sets {@link ToolCallContributor | `contributor`} to identify\nthe origin of the tool. For client-provided tools, the named client is\nresponsible for executing the tool once it reaches the `running` state\nand dispatching `session/toolCallComplete`. For MCP-served tools, the\nserver executes the call against the named `McpServerCustomization`.", + "description": "The default chat input-routing hint for this session changed.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallStart" + "$ref": "#/$defs/ActionType.SessionDefaultChatChanged" }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "New default chat URI, or `undefined` to clear the hint." + } + }, + "required": [ + "type" + ] + }, + "SessionTitleChangedAction": { + "type": "object", + "description": "Session title updated. Fired by the server when the title is auto-generated\nfrom conversation, or dispatched by a client to rename a session.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionTitleChanged" }, - "displayName": { + "title": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called. Absent for\nserver-side tools that are not contributed by a client or MCP server." + "description": "New title" } }, "required": [ - "turnId", - "toolCallId", "type", - "toolName", - "displayName" + "title" ] }, - "SessionToolCallDeltaAction": { + "SessionModelChangedAction": { "type": "object", - "description": "Streaming partial parameters for a tool call.", + "description": "Model changed for this session.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallDelta" - }, - "content": { - "type": "string", - "description": "Partial parameter content to append" + "$ref": "#/$defs/ActionType.SessionModelChanged" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Updated progress message" + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "New model selection" } }, "required": [ - "turnId", - "toolCallId", "type", - "content" + "model" ] }, - "SessionToolCallReadyAction": { + "SessionAgentChangedAction": { "type": "object", - "description": "Tool call parameters are complete, or a running tool requires re-confirmation.\n\nWhen dispatched for a `streaming` tool call, transitions to `pending-confirmation`\nor directly to `running` if `confirmed` is set.\n\nWhen dispatched for a `running` tool call (e.g. mid-execution permission needed),\ntransitions back to `pending-confirmation`. The `invocationMessage` and `_meta`\nSHOULD be updated to describe the specific confirmation needed. Clients use the\nstandard `session/toolCallConfirmed` flow to approve or deny.\n\nFor client-provided tools, the server typically sets `confirmed` to\n`'not-needed'` so the tool transitions directly to `running`, where the\nowning client can begin execution immediately.", + "description": "Custom agent selection changed for this session.\n\nOmitting `agent` (or setting it to `undefined`) clears the selection and\nresets the session to no selected custom agent (provider default behavior).\n\nWhen a turn is currently active, the server MUST defer the change until\nthe active turn completes, then apply it for the next turn (same rule as\n{@link SessionModelChangedAction | `session/modelChanged`}).", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallReady" - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do or what confirmation is needed" - }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ActionType.SessionAgentChanged" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "New agent selection, or `undefined` to clear the selection and reset the\nsession to no selected custom agent." + } + }, + "required": [ + "type" + ] + }, + "SessionIsReadChangedAction": { + "type": "object", + "description": "The read state of the session changed.\n\nDispatched by a client to mark a session as read (e.g. after viewing it)\nor unread (e.g. after new activity since the client last looked at it).", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionIsReadChanged" }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "string" - } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" + "isRead": { + "type": "boolean", + "description": "Whether the session has been read" + } + }, + "required": [ + "type", + "isRead" + ] + }, + "SessionIsArchivedChangedAction": { + "type": "object", + "description": "The archived state of the session changed.\n\nDispatched by a client to archive a session (e.g. the task is\ncomplete) or to unarchive it.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionIsArchivedChanged" }, - "editable": { + "isArchived": { "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + "description": "Whether the session is archived" + } + }, + "required": [ + "type", + "isArchived" + ] + }, + "SessionActivityChangedAction": { + "type": "object", + "description": "The activity description of the session changed.\n\nDispatched by the server to indicate what the session is currently doing\n(e.g. running a tool, thinking). Clear activity by setting it to `undefined`.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionActivityChanged" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "If set, the tool was auto-confirmed and transitions directly to `running`" + "activity": { + "type": "string", + "description": "Human-readable description of current activity, or `undefined` to clear" + } + }, + "required": [ + "type", + "activity" + ] + }, + "SessionChangesetsChangedAction": { + "type": "object", + "description": "The {@link Changeset | catalogue of changesets} the agent host\nadvertises for this session changed. Replaces\n{@link SessionState.changesets | `state.changesets`} entirely\n(full-replacement semantics) — set to `undefined` to clear the\ncatalogue.\n\nProducers dispatch this whenever entries are added or removed. The\nfan-out happens through this action so observers see catalogue\nmutations in the same {@link ChangesetAction | per-changeset} action\nstream they already follow for file-level updates.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionChangesetsChanged" }, - "options": { + "changesets": { "type": "array", "items": { - "$ref": "#/$defs/ConfirmationOption" + "$ref": "#/$defs/Changeset" }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "description": "New catalogue, or `undefined` to clear it" } }, "required": [ - "turnId", - "toolCallId", "type", - "invocationMessage" + "changesets" ] }, - "SessionToolCallApprovedAction": { + "SessionServerToolsChangedAction": { "type": "object", - "description": "Client approves a pending tool call. The tool transitions to `running`.", + "description": "Server tools for this session have changed.\n\nFull-replacement semantics: the `tools` array replaces the previous `serverTools` entirely.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallConfirmed" - }, - "approved": { - "description": "The tool call was approved" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed" - }, - "editedToolInput": { - "type": "string", - "description": "Edited tool input parameters, if the client modified them before confirming" + "$ref": "#/$defs/ActionType.SessionServerToolsChanged" }, - "selectedOptionId": { - "type": "string", - "description": "ID of the selected confirmation option, if the server provided options" + "tools": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolDefinition" + }, + "description": "Updated server tools list (full replacement)" } }, "required": [ - "turnId", - "toolCallId", "type", - "approved", - "confirmed" + "tools" ] }, - "SessionToolCallDeniedAction": { + "SessionActiveClientChangedAction": { "type": "object", - "description": "Client denies a pending tool call. The tool transitions to `cancelled`.\n\nFor client-provided tools, the owning client MUST dispatch this if it does\nnot recognize the tool or cannot execute it.", + "description": "The active client for this session has changed.\n\nA client dispatches this action with its own `SessionActiveClient` to claim\nthe active role, or with `null` to release it. The server SHOULD reject if\nanother client is already active. The server SHOULD automatically dispatch\nthis action with `activeClient: null` when the active client disconnects.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallConfirmed" - }, - "approved": { - "description": "The tool call was denied" + "$ref": "#/$defs/ActionType.SessionActiveClientChanged" }, - "reason": { + "activeClient": { "oneOf": [ { - "$ref": "#/$defs/ToolCallCancellationReason.Denied" + "$ref": "#/$defs/SessionActiveClient" }, - { - "$ref": "#/$defs/ToolCallCancellationReason.Skipped" - } + {} ], - "description": "Why the tool was cancelled" - }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" - }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional explanation for the denial" - }, - "selectedOptionId": { - "type": "string", - "description": "ID of the selected confirmation option, if the server provided options" + "description": "The new active client, or `null` to unset" } }, "required": [ - "turnId", - "toolCallId", "type", - "approved", - "reason" + "activeClient" ] }, - "SessionToolCallCompleteAction": { + "SessionActiveClientToolsChangedAction": { "type": "object", - "description": "Tool execution finished. Transitions to `completed` or `pending-result-confirmation`\nif `requiresResultConfirmation` is `true`.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action with the execution result. The server\nSHOULD reject this action if the dispatching client does not match `toolClientId`.\n\nServers waiting on a client tool call MAY time out after a reasonable duration\nif the implementing client disconnects or becomes unresponsive, and dispatch\nthis action with `result.success = false` and an appropriate error.", + "description": "The active client's tool list has changed.\n\nFull-replacement semantics: the `tools` array replaces the active client's\nprevious tools entirely. The server SHOULD reject if the dispatching client\nis not the current active client.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallComplete" - }, - "result": { - "$ref": "#/$defs/ToolCallResult", - "description": "Execution result" + "$ref": "#/$defs/ActionType.SessionActiveClientToolsChanged" }, - "requiresResultConfirmation": { - "type": "boolean", - "description": "If true, the result requires client approval before finalizing" + "tools": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolDefinition" + }, + "description": "Updated client tools list (full replacement)" } }, "required": [ - "turnId", - "toolCallId", "type", - "result" + "tools" ] }, - "SessionToolCallResultConfirmedAction": { + "SessionCustomizationsChangedAction": { "type": "object", - "description": "Client approves or denies a tool's result.\n\nIf `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`.", + "description": "The session's customizations have changed.\n\nFull-replacement semantics: the `customizations` array replaces the\nprevious `customizations` entirely.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" - }, - "toolCallId": { - "type": "string", - "description": "Tool call identifier" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." - }, "type": { - "$ref": "#/$defs/ActionType.SessionToolCallResultConfirmed" + "$ref": "#/$defs/ActionType.SessionCustomizationsChanged" }, - "approved": { - "type": "boolean", - "description": "Whether the result was approved" + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/Customization" + }, + "description": "Updated customization list (full replacement)." } }, "required": [ - "turnId", - "toolCallId", "type", - "approved" + "customizations" ] }, - "SessionToolCallContentChangedAction": { + "SessionCustomizationToggledAction": { "type": "object", - "description": "Partial content produced while a tool is still executing.\n\nReplaces the `content` array on the running tool call state. Clients can\nuse this to display live feedback (e.g. a terminal reference) before the\ntool completes.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action to stream intermediate content while\nexecuting. The server SHOULD reject this action if the dispatching client does\nnot match `toolClientId`.", + "description": "A client toggled a container customization on or off.\n\nTargets a top-level container (plugin or directory) by `id`. Only\ncontainers have an `enabled` flag; children are always active when\ntheir container is enabled. Is a no-op when no matching container is\nfound.", "properties": { - "turnId": { - "type": "string", - "description": "Turn identifier" + "type": { + "$ref": "#/$defs/ActionType.SessionCustomizationToggled" }, - "toolCallId": { + "id": { "type": "string", - "description": "Tool call identifier" + "description": "The id of the container to toggle." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + "enabled": { + "type": "boolean", + "description": "Whether to enable or disable the container." + } + }, + "required": [ + "type", + "id", + "enabled" + ] + }, + "SessionCustomizationUpdatedAction": { + "type": "object", + "description": "Upserts a top-level customization (plugin or directory).\n\nThe reducer locates the existing entry by `customization.id`:\n\n- If found, the entry is replaced entirely with `customization`,\n including its `children` array. To preserve existing children, the\n host must include them on the payload.\n- If not found, the entry is appended.", + "properties": { + "type": { + "$ref": "#/$defs/ActionType.SessionCustomizationUpdated" }, + "customization": { + "$ref": "#/$defs/Customization", + "description": "The customization to upsert (matched by `customization.id`)." + } + }, + "required": [ + "type", + "customization" + ] + }, + "SessionCustomizationRemovedAction": { + "type": "object", + "description": "Removes a customization by id.\n\nSearches every container and its children for the entry. If the entry\nis a container, its children are removed with it. Is a no-op when no\nmatching id is found.", + "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionToolCallContentChanged" + "$ref": "#/$defs/ActionType.SessionCustomizationRemoved" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "The current partial content for the running tool call" + "id": { + "type": "string", + "description": "The id of the customization to remove." } }, "required": [ - "turnId", - "toolCallId", "type", - "content" + "id" ] }, - "SessionTurnCompleteAction": { + "SessionMcpServerStateChangedAction": { "type": "object", - "description": "Turn finished — the assistant is idle.", + "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.state | `state`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatus.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerAuthRequiredState} for the rationale.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTurnComplete" + "$ref": "#/$defs/ActionType.SessionMcpServerStateChanged" }, - "turnId": { + "id": { "type": "string", - "description": "Turn identifier" + "description": "The id of the {@link McpServerCustomization} to update." + }, + "state": { + "$ref": "#/$defs/McpServerState", + "description": "The new lifecycle state." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatus.Ready | `Ready`})." } }, "required": [ "type", - "turnId" + "id", + "state" ] }, - "SessionTurnCancelledAction": { + "SessionConfigChangedAction": { "type": "object", - "description": "Turn was aborted; server stops processing.", + "description": "Client changed a mutable config value mid-session.\n\nOnly properties with `sessionMutable: true` in the config schema may be\nchanged. The server validates and broadcasts the action; the reducer merges\nthe new values into `state.config.values`.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTurnCancelled" + "$ref": "#/$defs/ActionType.SessionConfigChanged" }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Updated config values" + }, + "replace": { + "type": "boolean", + "description": "When `true`, replaces all config values instead of merging" } }, "required": [ "type", - "turnId" + "config" ] }, - "SessionErrorAction": { + "SessionMetaChangedAction": { "type": "object", - "description": "Error during turn processing.", + "description": "The session's `_meta` side-channel changed. Replaces `state._meta`\nentirely (full-replacement semantics). Producers SHOULD merge any\nkeys they wish to preserve into the new value before dispatching.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionError" - }, - "turnId": { - "type": "string", - "description": "Turn identifier" + "$ref": "#/$defs/ActionType.SessionMetaChanged" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "New `_meta` payload, or `undefined` to clear it" } }, "required": [ "type", - "turnId", - "error" + "_meta" ] }, - "SessionTitleChangedAction": { + "ToolCallActionBase": { "type": "object", - "description": "Session title updated. Fired by the server when the title is auto-generated\nfrom conversation, or dispatched by a client to rename a session.", + "description": "Base interface for all tool-call-scoped actions, carrying the common turn\nand tool call identifiers. The owning chat URI is identified by the\nenclosing {@link ActionEnvelope}'s `channel` field.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionTitleChanged" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "title": { + "toolCallId": { "type": "string", - "description": "New title" + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." } }, "required": [ - "type", - "title" + "turnId", + "toolCallId" ] }, - "SessionUsageAction": { + "ChatTurnStartedAction": { "type": "object", - "description": "Token usage report for a turn.", + "description": "A new message has been sent to the agent, and a new turn starts.\n\nA client is only allowed to send {@link MessageKind.User} messages.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionUsage" + "$ref": "#/$defs/ActionType.ChatTurnStarted" }, "turnId": { "type": "string", "description": "Turn identifier" }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage data" + "message": { + "$ref": "#/$defs/Message", + "description": "The new message" + }, + "queuedMessageId": { + "type": "string", + "description": "If this turn was auto-started from a queued message, the ID of that message" } }, "required": [ "type", "turnId", - "usage" + "message" ] }, - "SessionReasoningAction": { + "ChatDeltaAction": { "type": "object", - "description": "Reasoning/thinking text from the model, appended to a specific reasoning response part.\n\nThe server MUST first emit a `session/responsePart` to create the target\nreasoning part, then use this action to append text to it.", + "description": "Streaming text chunk from the assistant, appended to a specific response part.\n\nThe server MUST first emit a `chat/responsePart` to create the target\npart (markdown or reasoning), then use this action to append text to it.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionReasoning" + "$ref": "#/$defs/ActionType.ChatDelta" }, "turnId": { "type": "string", @@ -5798,11 +5922,11 @@ }, "partId": { "type": "string", - "description": "Identifier of the reasoning response part to append to" + "description": "Identifier of the response part to append to" }, "content": { "type": "string", - "description": "Reasoning text chunk" + "description": "Text chunk" } }, "required": [ @@ -5812,320 +5936,485 @@ "content" ] }, - "SessionModelChangedAction": { + "ChatResponsePartAction": { "type": "object", - "description": "Model changed for this session.", + "description": "Structured content appended to the response.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionModelChanged" + "$ref": "#/$defs/ActionType.ChatResponsePart" }, - "model": { - "$ref": "#/$defs/ModelSelection", - "description": "New model selection" + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "part": { + "$ref": "#/$defs/ResponsePart", + "description": "Response part (markdown or content ref)" } }, "required": [ "type", - "model" + "turnId", + "part" ] }, - "SessionAgentChangedAction": { + "ChatToolCallStartAction": { "type": "object", - "description": "Custom agent selection changed for this session.\n\nOmitting `agent` (or setting it to `undefined`) clears the selection and\nresets the session to no selected custom agent (provider default behavior).\n\nWhen a turn is currently active, the server MUST defer the change until\nthe active turn completes, then apply it for the next turn (same rule as\n{@link SessionModelChangedAction | `session/modelChanged`}).", + "description": "A tool call begins — parameters are streaming from the LM.\n\nThe server sets {@link ToolCallContributor | `contributor`} to identify\nthe origin of the tool. For client-provided tools, the named client is\nresponsible for executing the tool once it reaches the `running` state\nand dispatching `chat/toolCallComplete`. For MCP-served tools, the\nserver executes the call against the named `McpServerCustomization`.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionAgentChanged" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "agent": { - "$ref": "#/$defs/AgentSelection", - "description": "New agent selection, or `undefined` to clear the selection and reset the\nsession to no selected custom agent." - } - }, - "required": [ - "type" - ] - }, - "SessionIsReadChangedAction": { - "type": "object", - "description": "The read state of the session changed.\n\nDispatched by a client to mark a session as read (e.g. after viewing it)\nor unread (e.g. after new activity since the client last looked at it).", - "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionIsReadChanged" + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." }, - "isRead": { - "type": "boolean", - "description": "Whether the session has been read" - } - }, - "required": [ - "type", - "isRead" - ] - }, - "SessionIsArchivedChangedAction": { - "type": "object", - "description": "The archived state of the session changed.\n\nDispatched by a client to archive a session (e.g. the task is\ncomplete) or to unarchive it.", - "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionIsArchivedChanged" + "$ref": "#/$defs/ActionType.ChatToolCallStart" }, - "isArchived": { - "type": "boolean", - "description": "Whether the session is archived" + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called. Absent for\nserver-side tools that are not contributed by a client or MCP server." } }, "required": [ + "turnId", + "toolCallId", "type", - "isArchived" + "toolName", + "displayName" ] }, - "SessionActivityChangedAction": { + "ChatToolCallDeltaAction": { "type": "object", - "description": "The activity description of the session changed.\n\nDispatched by the server to indicate what the session is currently doing\n(e.g. running a tool, thinking). Clear activity by setting it to `undefined`.", + "description": "Streaming partial parameters for a tool call.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionActivityChanged" + "$ref": "#/$defs/ActionType.ChatToolCallDelta" }, - "activity": { + "content": { "type": "string", - "description": "Human-readable description of current activity, or `undefined` to clear" + "description": "Partial parameter content to append" + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Updated progress message" } }, "required": [ + "turnId", + "toolCallId", "type", - "activity" + "content" ] }, - "SessionChangesetsChangedAction": { + "ChatToolCallReadyAction": { "type": "object", - "description": "The {@link Changeset | catalogue of changesets} the agent host\nadvertises for this session changed. Replaces\n{@link SessionState.changesets | `state.changesets`} entirely\n(full-replacement semantics) — set to `undefined` to clear the\ncatalogue.\n\nProducers dispatch this whenever entries are added or removed. The\nfan-out happens through this action so observers see catalogue\nmutations in the same {@link ChangesetAction | per-changeset} action\nstream they already follow for file-level updates.", + "description": "Tool call parameters are complete, or a running tool requires re-confirmation.\n\nWhen dispatched for a `streaming` tool call, transitions to `pending-confirmation`\nor directly to `running` if `confirmed` is set.\n\nWhen dispatched for a `running` tool call (e.g. mid-execution permission needed),\ntransitions back to `pending-confirmation`. The `invocationMessage` and `_meta`\nSHOULD be updated to describe the specific confirmation needed. Clients use the\nstandard `chat/toolCallConfirmed` flow to approve or deny.\n\nFor client-provided tools, the server typically sets `confirmed` to\n`'not-needed'` so the tool transitions directly to `running`, where the\nowning client can begin execution immediately.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionChangesetsChanged" + "$ref": "#/$defs/ActionType.ChatToolCallReady" }, - "changesets": { + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do or what confirmation is needed" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "edits": { + "type": "object", + "properties": { + "items": { + "type": "string" + } + }, + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" + }, + "editable": { + "type": "boolean", + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "If set, the tool was auto-confirmed and transitions directly to `running`" + }, + "options": { "type": "array", "items": { - "$ref": "#/$defs/Changeset" + "$ref": "#/$defs/ConfirmationOption" }, - "description": "New catalogue, or `undefined` to clear it" + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." } }, "required": [ + "turnId", + "toolCallId", "type", - "changesets" + "invocationMessage" ] }, - "SessionServerToolsChangedAction": { + "ChatToolCallApprovedAction": { "type": "object", - "description": "Server tools for this session have changed.\n\nFull-replacement semantics: the `tools` array replaces the previous `serverTools` entirely.", + "description": "Client approves a pending tool call. The tool transitions to `running`.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionServerToolsChanged" + "$ref": "#/$defs/ActionType.ChatToolCallConfirmed" }, - "tools": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolDefinition" - }, - "description": "Updated server tools list (full replacement)" + "approved": { + "description": "The tool call was approved" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed" + }, + "editedToolInput": { + "type": "string", + "description": "Edited tool input parameters, if the client modified them before confirming" + }, + "selectedOptionId": { + "type": "string", + "description": "ID of the selected confirmation option, if the server provided options" } }, "required": [ + "turnId", + "toolCallId", "type", - "tools" + "approved", + "confirmed" ] }, - "SessionActiveClientChangedAction": { + "ChatToolCallDeniedAction": { "type": "object", - "description": "The active client for this session has changed.\n\nA client dispatches this action with its own `SessionActiveClient` to claim\nthe active role, or with `null` to release it. The server SHOULD reject if\nanother client is already active. The server SHOULD automatically dispatch\nthis action with `activeClient: null` when the active client disconnects.", + "description": "Client denies a pending tool call. The tool transitions to `cancelled`.\n\nFor client-provided tools, the owning client MUST dispatch this if it does\nnot recognize the tool or cannot execute it.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionActiveClientChanged" + "$ref": "#/$defs/ActionType.ChatToolCallConfirmed" }, - "activeClient": { + "approved": { + "description": "The tool call was denied" + }, + "reason": { "oneOf": [ { - "$ref": "#/$defs/SessionActiveClient" + "$ref": "#/$defs/ToolCallCancellationReason.Denied" }, - {} + { + "$ref": "#/$defs/ToolCallCancellationReason.Skipped" + } ], - "description": "The new active client, or `null` to unset" + "description": "Why the tool was cancelled" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional explanation for the denial" + }, + "selectedOptionId": { + "type": "string", + "description": "ID of the selected confirmation option, if the server provided options" } }, "required": [ + "turnId", + "toolCallId", "type", - "activeClient" + "approved", + "reason" ] }, - "SessionActiveClientToolsChangedAction": { + "ChatToolCallCompleteAction": { "type": "object", - "description": "The active client's tool list has changed.\n\nFull-replacement semantics: the `tools` array replaces the active client's\nprevious tools entirely. The server SHOULD reject if the dispatching client\nis not the current active client.", + "description": "Tool execution finished. Transitions to `completed` or `pending-result-confirmation`\nif `requiresResultConfirmation` is `true`.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action with the execution result. The server\nSHOULD reject this action if the dispatching client does not match `toolClientId`.\n\nServers waiting on a client tool call MAY time out after a reasonable duration\nif the implementing client disconnects or becomes unresponsive, and dispatch\nthis action with `result.success = false` and an appropriate error.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionActiveClientToolsChanged" + "$ref": "#/$defs/ActionType.ChatToolCallComplete" }, - "tools": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolDefinition" - }, - "description": "Updated client tools list (full replacement)" + "result": { + "$ref": "#/$defs/ToolCallResult", + "description": "Execution result" + }, + "requiresResultConfirmation": { + "type": "boolean", + "description": "If true, the result requires client approval before finalizing" } }, "required": [ + "turnId", + "toolCallId", "type", - "tools" + "result" ] }, - "SessionCustomizationsChangedAction": { + "ChatToolCallResultConfirmedAction": { "type": "object", - "description": "The session's customizations have changed.\n\nFull-replacement semantics: the `customizations` array replaces the\nprevious `customizations` entirely.", + "description": "Client approves or denies a tool's result.\n\nIf `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`.", "properties": { + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "toolCallId": { + "type": "string", + "description": "Tool call identifier" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationsChanged" + "$ref": "#/$defs/ActionType.ChatToolCallResultConfirmed" }, - "customizations": { - "type": "array", - "items": { - "$ref": "#/$defs/Customization" - }, - "description": "Updated customization list (full replacement)." + "approved": { + "type": "boolean", + "description": "Whether the result was approved" } }, "required": [ + "turnId", + "toolCallId", "type", - "customizations" + "approved" ] }, - "SessionCustomizationToggledAction": { + "ChatToolCallContentChangedAction": { "type": "object", - "description": "A client toggled a container customization on or off.\n\nTargets a top-level container (plugin or directory) by `id`. Only\ncontainers have an `enabled` flag; children are always active when\ntheir container is enabled. Is a no-op when no matching container is\nfound.", + "description": "Partial content produced while a tool is still executing.\n\nReplaces the `content` array on the running tool call state. Clients can\nuse this to display live feedback (e.g. a terminal reference) before the\ntool completes.\n\nFor client-provided tools (where `toolClientId` is set on the tool call state),\nthe owning client dispatches this action to stream intermediate content while\nexecuting. The server SHOULD reject this action if the dispatching client does\nnot match `toolClientId`.", "properties": { - "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationToggled" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "id": { + "toolCallId": { "type": "string", - "description": "The id of the container to toggle." + "description": "Tool call identifier" }, - "enabled": { - "type": "boolean", - "description": "Whether to enable or disable the container." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `ptyTerminal` key with `{ input: string; output: string }`\nindicates the tool operated on a terminal (both `input` and `output` may\ncontain escape sequences)." + }, + "type": { + "$ref": "#/$defs/ActionType.ChatToolCallContentChanged" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "The current partial content for the running tool call" } }, "required": [ + "turnId", + "toolCallId", "type", - "id", - "enabled" + "content" ] }, - "SessionCustomizationUpdatedAction": { + "ChatTurnCompleteAction": { "type": "object", - "description": "Upserts a top-level customization (plugin or directory).\n\nThe reducer locates the existing entry by `customization.id`:\n\n- If found, the entry is replaced entirely with `customization`,\n including its `children` array. To preserve existing children, the\n host must include them on the payload.\n- If not found, the entry is appended.", + "description": "Turn finished — the assistant is idle.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationUpdated" + "$ref": "#/$defs/ActionType.ChatTurnComplete" }, - "customization": { - "$ref": "#/$defs/Customization", - "description": "The customization to upsert (matched by `customization.id`)." + "turnId": { + "type": "string", + "description": "Turn identifier" } }, "required": [ "type", - "customization" + "turnId" ] }, - "SessionCustomizationRemovedAction": { + "ChatTurnCancelledAction": { "type": "object", - "description": "Removes a customization by id.\n\nSearches every container and its children for the entry. If the entry\nis a container, its children are removed with it. Is a no-op when no\nmatching id is found.", + "description": "Turn was aborted; server stops processing.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionCustomizationRemoved" + "$ref": "#/$defs/ActionType.ChatTurnCancelled" }, - "id": { + "turnId": { "type": "string", - "description": "The id of the customization to remove." + "description": "Turn identifier" } }, "required": [ "type", - "id" + "turnId" ] }, - "SessionMcpServerStateChangedAction": { + "ChatErrorAction": { "type": "object", - "description": "Updates the runtime fields of an existing\n{@link McpServerCustomization} — narrow alternative to\n{@link SessionCustomizationUpdatedAction} for the high-frequency\n`starting` ↔ `ready` ↔ `authRequired` transitions.\n\nLocates the target entry by `id`, searching both the top-level\ncustomization list and the `children` array of every container.\nReplaces the entry's {@link McpServerCustomization.state | `state`}\nand {@link McpServerCustomization.channel | `channel`}\n(full-replacement semantics: omit `channel` to clear an existing\nchannel URI). Other fields of the customization are preserved.\n\nIs a no-op when no matching `McpServerCustomization` is found. To\nupdate any other field (name, icons, `mcpApp` capabilities, etc.) use\n{@link SessionCustomizationUpdatedAction} instead.\n\nWhen the transition is to {@link McpServerStatus.AuthRequired}\nbecause of a request issued mid-turn, the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session — see\n{@link McpServerAuthRequiredState} for the rationale.", + "description": "Error during turn processing.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionMcpServerStateChanged" + "$ref": "#/$defs/ActionType.ChatError" }, - "id": { + "turnId": { "type": "string", - "description": "The id of the {@link McpServerCustomization} to update." - }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "The new lifecycle state." + "description": "Turn identifier" }, - "channel": { - "$ref": "#/$defs/URI", - "description": "Updated `mcp://` side-channel URI. Full-replacement: omit to clear\nan existing channel (typical when leaving\n{@link McpServerStatus.Ready | `Ready`})." + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details" } }, "required": [ "type", - "id", - "state" + "turnId", + "error" ] }, - "SessionConfigChangedAction": { + "ChatUsageAction": { "type": "object", - "description": "Client changed a mutable config value mid-session.\n\nOnly properties with `sessionMutable: true` in the config schema may be\nchanged. The server validates and broadcasts the action; the reducer merges\nthe new values into `state.config.values`.", + "description": "Token usage report for a turn.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionConfigChanged" + "$ref": "#/$defs/ActionType.ChatUsage" }, - "config": { - "type": "object", - "additionalProperties": {}, - "description": "Updated config values" + "turnId": { + "type": "string", + "description": "Turn identifier" }, - "replace": { - "type": "boolean", - "description": "When `true`, replaces all config values instead of merging" + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage data" } }, "required": [ "type", - "config" + "turnId", + "usage" ] }, - "SessionMetaChangedAction": { + "ChatReasoningAction": { "type": "object", - "description": "The session's `_meta` side-channel changed. Replaces `state._meta`\nentirely (full-replacement semantics). Producers SHOULD merge any\nkeys they wish to preserve into the new value before dispatching.", + "description": "Reasoning/thinking text from the model, appended to a specific reasoning response part.\n\nThe server MUST first emit a `chat/responsePart` to create the target\nreasoning part, then use this action to append text to it.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionMetaChanged" + "$ref": "#/$defs/ActionType.ChatReasoning" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "New `_meta` payload, or `undefined` to clear it" + "turnId": { + "type": "string", + "description": "Turn identifier" + }, + "partId": { + "type": "string", + "description": "Identifier of the reasoning response part to append to" + }, + "content": { + "type": "string", + "description": "Reasoning text chunk" } }, "required": [ "type", - "_meta" + "turnId", + "partId", + "content" ] }, - "SessionTruncatedAction": { + "ChatTruncatedAction": { "type": "object", - "description": "Truncates a session's history. If `turnId` is provided, all turns after that\nturn are removed and the specified turn is kept. If `turnId` is omitted, all\nturns are removed.\n\nIf there is an active turn it is silently dropped and the session status\nreturns to `idle`.\n\nCommon use-case: truncate old data then dispatch a new\n`session/turnStarted` with an edited message.", + "description": "Truncates a session's history. If `turnId` is provided, all turns after that\nturn are removed and the specified turn is kept. If `turnId` is omitted, all\nturns are removed.\n\nIf there is an active turn it is silently dropped and the chat status\nreturns to `idle`.\n\nCommon use-case: truncate old data then dispatch a new\n`chat/turnStarted` with an edited message.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionTruncated" + "$ref": "#/$defs/ActionType.ChatTruncated" }, "turnId": { "type": "string", @@ -6136,12 +6425,12 @@ "type" ] }, - "SessionPendingMessageSetAction": { + "ChatPendingMessageSetAction": { "type": "object", - "description": "A pending message was set (upsert semantics: creates or replaces).\n\nFor steering messages, this always replaces the single steering message.\nFor queued messages, if a message with the given `id` already exists it is\nupdated in place; otherwise it is appended to the queue. If the session is\nidle when a queued message is set, the server SHOULD immediately consume it\nand start a new turn.\n\nA client is only allowed to send {@link MessageKind.User} messages.", + "description": "A pending message was set (upsert semantics: creates or replaces).\n\nFor steering messages, this always replaces the single steering message.\nFor queued messages, if a message with the given `id` already exists it is\nupdated in place; otherwise it is appended to the queue. If the chat is\nidle when a queued message is set, the server SHOULD immediately consume it\nand start a new turn.\n\nA client is only allowed to send {@link MessageKind.User} messages.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionPendingMessageSet" + "$ref": "#/$defs/ActionType.ChatPendingMessageSet" }, "kind": { "$ref": "#/$defs/PendingMessageKind", @@ -6163,12 +6452,12 @@ "message" ] }, - "SessionPendingMessageRemovedAction": { + "ChatPendingMessageRemovedAction": { "type": "object", "description": "A pending message was removed (steering or queued).\n\nDispatched by clients to cancel a pending message, or by the server when\nit consumes a message (e.g. starting a turn from a queued message or\ninjecting a steering message into the current turn).", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionPendingMessageRemoved" + "$ref": "#/$defs/ActionType.ChatPendingMessageRemoved" }, "kind": { "$ref": "#/$defs/PendingMessageKind", @@ -6185,12 +6474,12 @@ "id" ] }, - "SessionQueuedMessagesReorderedAction": { + "ChatQueuedMessagesReorderedAction": { "type": "object", "description": "Reorder the queued messages.\n\nThe `order` array contains the IDs of queued messages in their new\ndesired order. IDs not present in the current queue are ignored.\nQueued messages whose IDs are absent from `order` are appended at\nthe end in their original relative order (so a client with a stale\nview of the queue never silently drops messages).", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionQueuedMessagesReordered" + "$ref": "#/$defs/ActionType.ChatQueuedMessagesReordered" }, "order": { "type": "array", @@ -6205,15 +6494,15 @@ "order" ] }, - "SessionInputRequestedAction": { + "ChatInputRequestedAction": { "type": "object", "description": "A session requested input from the user.\n\nFull-request upsert semantics: the `request` replaces any existing request\nwith the same `id`, or is appended if it is new. Answer drafts are preserved\nunless `request.answers` is provided.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionInputRequested" + "$ref": "#/$defs/ActionType.ChatInputRequested" }, "request": { - "$ref": "#/$defs/SessionInputRequest", + "$ref": "#/$defs/ChatInputRequest", "description": "Input request to create or replace" } }, @@ -6222,12 +6511,12 @@ "request" ] }, - "SessionInputAnswerChangedAction": { + "ChatInputAnswerChangedAction": { "type": "object", "description": "A client updated, submitted, skipped, or removed a single in-progress answer.\n\nDispatching with `answer: undefined` removes that question's answer draft.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionInputAnswerChanged" + "$ref": "#/$defs/ActionType.ChatInputAnswerChanged" }, "requestId": { "type": "string", @@ -6238,7 +6527,7 @@ "description": "Question identifier within the input request" }, "answer": { - "$ref": "#/$defs/SessionInputAnswer", + "$ref": "#/$defs/ChatInputAnswer", "description": "Updated answer, or `undefined` to clear an answer draft" } }, @@ -6248,25 +6537,25 @@ "questionId" ] }, - "SessionInputCompletedAction": { + "ChatInputCompletedAction": { "type": "object", "description": "A client accepted, declined, or cancelled a session input request.\n\nIf accepted, the server uses `answers` (when provided) plus the request's\nsynced answer state to resume the blocked operation.", "properties": { "type": { - "$ref": "#/$defs/ActionType.SessionInputCompleted" + "$ref": "#/$defs/ActionType.ChatInputCompleted" }, "requestId": { "type": "string", "description": "Input request identifier" }, "response": { - "$ref": "#/$defs/SessionInputResponseKind", + "$ref": "#/$defs/ChatInputResponseKind", "description": "Completion outcome" }, "answers": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/SessionInputAnswer" + "$ref": "#/$defs/ChatInputAnswer" }, "description": "Optional final answer replacement, keyed by question ID" } diff --git a/schema/errors.schema.json b/schema/errors.schema.json index e3e5ab92..658b3207 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -484,7 +484,7 @@ "properties": { "resource": { "$ref": "#/$defs/URI", - "description": "The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`)" + "description": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" }, "state": { "oneOf": [ @@ -682,24 +682,6 @@ "values" ] }, - "PendingMessage": { - "type": "object", - "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this pending message" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that will start the next turn" - } - }, - "required": [ - "id", - "message" - ] - }, "SessionState": { "type": "object", "description": "Full state for a single session, loaded when a client subscribes to the session's URI.", @@ -727,34 +709,16 @@ "$ref": "#/$defs/SessionActiveClient", "description": "The client currently providing tools and interactive capabilities to this session" }, - "turns": { - "type": "array", - "items": { - "$ref": "#/$defs/Turn" - }, - "description": "Completed turns" - }, - "activeTurn": { - "$ref": "#/$defs/ActiveTurn", - "description": "Currently in-progress turn" - }, - "steeringMessage": { - "$ref": "#/$defs/PendingMessage", - "description": "Message to inject into the current turn at a convenient point" - }, - "queuedMessages": { + "chats": { "type": "array", "items": { - "$ref": "#/$defs/PendingMessage" + "$ref": "#/$defs/ChatSummary" }, - "description": "Messages to send automatically as new turns after the current turn finishes" + "description": "Catalog of chats in this session." }, - "inputRequests": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputRequest" - }, - "description": "Requests for user input that are currently blocking or informing session progress" + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "The chat that receives input when the user addresses the session without\nselecting a specific chat. This is a UI routing hint, not a hierarchy\nmarker — chats remain equal peers at the protocol level. Hosts MAY change\nthis over the session's lifetime." }, "config": { "$ref": "#/$defs/SessionConfigState", @@ -783,7 +747,7 @@ "required": [ "summary", "lifecycle", - "turns" + "chats" ] }, "SessionActiveClient": { @@ -838,6 +802,7 @@ }, "SessionSummary": { "type": "object", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `model` / `agent`: the session-level selection. Per-chat overrides are\n surfaced on individual {@link ChatSummary} entries, not aggregated up.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "resource": { "$ref": "#/$defs/URI", @@ -881,7 +846,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory URI for this session" + "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, "changes": { "$ref": "#/$defs/ChangesSummary", @@ -1065,2451 +1030,2600 @@ "values" ] }, - "SessionInputOption": { + "ToolDefinition": { "type": "object", - "description": "A choice in a select-style question.", + "description": "Describes a tool available in a session, provided by either the server or the active client.", "properties": { - "id": { + "name": { "type": "string", - "description": "Stable option identifier; for MCP enum values this is the enum string" + "description": "Unique tool identifier" }, - "label": { + "title": { "type": "string", - "description": "Display label" + "description": "Human-readable display name" }, "description": { "type": "string", - "description": "Optional secondary text" + "description": "Description of what the tool does" }, - "recommended": { - "type": "boolean", - "description": "Whether this option is the recommended/default choice" + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + }, + "outputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + }, + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Behavioral hints about the tool. All properties are advisory." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "id", - "label" + "name" ] }, - "SessionInputQuestionBase": { + "ToolAnnotations": { "type": "object", + "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, "title": { "type": "string", - "description": "Short display title" + "description": "Alternate human-readable title" }, - "message": { - "type": "string", - "description": "Prompt shown to the user" + "readOnlyHint": { + "type": "boolean", + "description": "Tool does not modify its environment (default: false)" }, - "required": { + "destructiveHint": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Tool may perform destructive updates (default: true)" + }, + "idempotentHint": { + "type": "boolean", + "description": "Repeated calls with the same arguments have no additional effect (default: false)" + }, + "openWorldHint": { + "type": "boolean", + "description": "Tool may interact with external entities (default: true)" } - }, - "required": [ - "id", - "message" - ] + } }, - "SessionInputTextQuestion": { + "CustomizationBase": { "type": "object", - "description": "Text question within a session input request.", + "description": "Fields shared by every customization variant.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Text" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "format": { + "name": { "type": "string", - "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" - }, - "min": { - "type": "number", - "description": "Minimum string length" + "description": "Human-readable name." }, - "max": { - "type": "number", - "description": "Maximum string length" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "defaultValue": { - "type": "string", - "description": "Default text" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." } }, "required": [ "id", - "message", - "kind" + "uri", + "name" ] }, - "SessionInputNumberQuestion": { + "CustomizationLoadingState": { "type": "object", - "description": "Numeric question within a session input request.", + "description": "Container is being loaded by the host.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputQuestionKind.Number" - }, - { - "$ref": "#/$defs/SessionInputQuestionKind.Integer" - } - ] - }, - "min": { - "type": "number", - "description": "Minimum value" - }, - "max": { - "type": "number", - "description": "Maximum value" - }, - "defaultValue": { - "type": "number", - "description": "Default numeric value" + "$ref": "#/$defs/CustomizationLoadStatus.Loading" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputBooleanQuestion": { + "CustomizationLoadedState": { "type": "object", - "description": "Boolean question within a session input request.", + "description": "Container loaded successfully.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Boolean" - }, - "defaultValue": { - "type": "boolean", - "description": "Default boolean value" + "$ref": "#/$defs/CustomizationLoadStatus.Loaded" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputSingleSelectQuestion": { + "CustomizationDegradedState": { "type": "object", - "description": "Single-select question within a session input request.", + "description": "Container partially loaded but has warnings.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" + "kind": { + "$ref": "#/$defs/CustomizationLoadStatus.Degraded" }, "message": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, + "description": "Human-readable description of the warning." + } + }, + "required": [ + "kind", + "message" + ] + }, + "CustomizationErrorState": { + "type": "object", + "description": "Container failed to load.", + "properties": { "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.SingleSelect" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputOption" - }, - "description": "Options the user may select from" + "$ref": "#/$defs/CustomizationLoadStatus.Error" }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text instead of selecting an option" + "message": { + "type": "string", + "description": "Human-readable error message." } }, "required": [ - "id", - "message", "kind", - "options" + "message" ] }, - "SessionInputMultiSelectQuestion": { + "ContainerCustomizationBase": { "type": "object", - "description": "Multi-select question within a session input request.", + "description": "Fields shared by container customizations.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "title": { - "type": "string", - "description": "Short display title" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "message": { + "name": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.MultiSelect" + "description": "Human-readable name." }, - "options": { + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputOption" + "$ref": "#/$defs/Icon" }, - "description": "Options the user may select from" + "description": "Icons for UI display." }, - "allowFreeformInput": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { "type": "boolean", - "description": "Whether the user may enter text in addition to selecting options" + "description": "Whether this container is currently enabled." }, - "min": { - "type": "number", - "description": "Minimum selected item count" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "max": { - "type": "number", - "description": "Maximum selected item count" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." } }, "required": [ "id", - "message", - "kind", - "options" + "uri", + "name", + "enabled" ] }, - "SessionInputRequest": { + "PluginCustomization": { "type": "object", - "description": "A live request for user input.\n\nThe server creates or replaces requests with `session/inputRequested`.\nClients sync drafts with `session/inputAnswerChanged` and complete requests\nwith `session/inputCompleted`.", + "description": "An [Open Plugins](https://open-plugins.com/) plugin.", "properties": { "id": { "type": "string", - "description": "Stable request identifier" - }, - "message": { - "type": "string", - "description": "Display message for the request as a whole" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "url": { + "uri": { "$ref": "#/$defs/URI", - "description": "URL the user should review or open, for URL-style elicitations" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "questions": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputQuestion" - }, - "description": "Ordered questions to ask the user" - }, - "answers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/SessionInputAnswer" + "$ref": "#/$defs/Icon" }, - "description": "Current draft or submitted answers, keyed by question ID" - } - }, - "required": [ - "id" - ] - }, - "SessionInputTextAnswerValue": { - "type": "object", - "description": "Value captured for one answer.", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Text" + "description": "Icons for UI display." }, - "value": { - "type": "string" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputNumberAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Number" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "value": { - "type": "number" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputBooleanAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Boolean" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "value": { - "type": "boolean" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputSelectedAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Selected" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "value": { - "type": "string" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "freeformValues": { + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered instead of selecting an option" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputSelectedManyAnswerValue": { + "ClientPluginCustomization": { "type": "object", + "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.SelectedMany" + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "value": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "type": "string" - } + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "freeformValues": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered in addition to selected options" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" + }, + "nonce": { + "type": "string", + "description": "Opaque version token used by the host to detect changes." } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputAnswered": { - "type": "object", - "properties": { - "state": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputAnswerState.Draft" - }, - { - "$ref": "#/$defs/SessionInputAnswerState.Submitted" - } - ], - "description": "Answer state" - }, - "value": { - "$ref": "#/$defs/SessionInputAnswerValue", - "description": "Answer value" - } - }, - "required": [ - "state", - "value" - ] - }, - "SessionInputSkipped": { - "type": "object", - "properties": { - "state": { - "$ref": "#/$defs/SessionInputAnswerState.Skipped", - "description": "Answer state" - }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form reason or value captured while skipping, if any" - } - }, - "required": [ - "state" - ] - }, - "Turn": { + "DirectoryCustomization": { "type": "object", - "description": "A completed request/response cycle.", + "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "responseParts": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/Icon" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." + "description": "Icons for UI display." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "state": { - "$ref": "#/$defs/TurnState", - "description": "How the turn ended" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details if state is `'error'`" - } - }, - "required": [ - "id", - "message", - "responseParts", - "usage", - "state" - ] - }, - "ActiveTurn": { - "type": "object", - "description": "An in-progress turn — the assistant is actively streaming.", - "properties": { - "id": { + "clientId": { "type": "string", - "description": "Turn identifier" + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "responseParts": { + "children": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/ChildCustomization" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "type": { + "$ref": "#/$defs/CustomizationType.Directory" + }, + "contents": { + "$ref": "#/$defs/ChildCustomizationType", + "description": "Which child customization type this directory holds." + }, + "writable": { + "type": "boolean", + "description": "Whether clients may write into this directory." } }, "required": [ "id", - "message", - "responseParts", - "usage" + "uri", + "name", + "enabled", + "type", + "contents", + "writable" ] }, - "Message": { + "AgentCustomization": { "type": "object", - "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", "properties": { - "text": { + "id": { "type": "string", - "description": "Message text" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "origin": { - "type": "object", - "properties": { - "kind": { - "type": "string" - } - }, - "required": [ - "kind" - ], - "description": "The origin of the message" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "attachments": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/MessageAttachment" + "$ref": "#/$defs/Icon" }, - "description": "File/selection attachments" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." - } - }, - "required": [ - "text", - "origin" - ] - }, - "MessageAttachmentBase": { - "type": "object", - "description": "Common fields shared by all {@link MessageAttachment} variants.", - "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Icons for UI display." }, "range": { "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "displayKind": { + "type": { + "$ref": "#/$defs/CustomizationType.Agent" + }, + "description": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "label" + "id", + "uri", + "name", + "type" ] }, - "SimpleMessageAttachment": { + "SkillCustomization": { "type": "object", - "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", + "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "type": { - "$ref": "#/$defs/MessageAttachmentKind.Simple", - "description": "Discriminant" + "$ref": "#/$defs/CustomizationType.Skill" }, - "modelRepresentation": { + "description": { "type": "string", - "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." } }, "required": [ - "label", + "id", + "uri", + "name", "type" ] }, - "MessageEmbeddedResourceAttachment": { + "PromptCustomization": { "type": "object", - "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "description": "A prompt contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", - "description": "Discriminant" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "data": { - "type": "string", - "description": "Base64-encoded binary data" + "type": { + "$ref": "#/$defs/CustomizationType.Prompt" }, - "contentType": { + "description": { "type": "string", - "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" - }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + "description": "Short description of what the prompt does." } }, "required": [ - "label", - "type", - "data", - "contentType" + "id", + "uri", + "name", + "type" ] }, - "MessageResourceAttachment": { + "RuleCustomization": { "type": "object", - "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", + "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "type": { + "$ref": "#/$defs/CustomizationType.Rule" }, - "contentType": { + "description": { "type": "string", - "description": "Content MIME type" + "description": "Description of what the rule enforces." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Resource", - "description": "Discriminant" + "alwaysApply": { + "type": "boolean", + "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + "globs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." } }, "required": [ - "label", + "id", "uri", + "name", "type" ] }, - "MessageAnnotationsAttachment": { + "HookCustomization": { "type": "object", - "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", + "description": "A hook manifest contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." - }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Annotations", - "description": "Discriminant" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + "description": "Human-readable name." }, - "annotationIds": { + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." - } - }, - "required": [ - "label", - "type", - "resource" - ] - }, - "MarkdownResponsePart": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.Markdown", - "description": "Discriminant" + "description": "Icons for UI display." }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/delta` to target this part for content appends" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "content": { - "type": "string", - "description": "Markdown content" + "type": { + "$ref": "#/$defs/CustomizationType.Hook" } }, "required": [ - "kind", "id", - "content" + "uri", + "name", + "type" ] }, - "ResourceReponsePart": { + "McpServerCustomization": { "type": "object", - "description": "A content part that's a reference to large content stored outside the state tree.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, "uri": { "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "contentType": { + "name": { "type": "string", - "description": "Content MIME type" + "description": "Human-readable name." }, - "kind": { - "$ref": "#/$defs/ResponsePartKind.ContentRef", - "description": "Discriminant" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "type": { + "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ + "id", "uri", - "kind" + "name", + "type", + "enabled", + "state" ] }, - "ToolCallResponsePart": { + "McpServerCustomizationApps": { "type": "object", - "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.ToolCall", - "description": "Discriminant" - }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "Full tool call lifecycle state" + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." } }, "required": [ - "kind", - "toolCall" + "capabilities" ] }, - "ReasoningResponsePart": { + "AhpMcpUiHostCapabilities": { "type": "object", - "description": "Reasoning/thinking content from the model.", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.Reasoning", - "description": "Discriminant" + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/reasoning` to target this part for content appends" + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." }, - "content": { - "type": "string", - "description": "Accumulated reasoning text" + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." } - }, - "required": [ - "kind", - "id", - "content" - ] + } }, - "SystemNotificationResponsePart": { + "McpServerStartingState": { "type": "object", - "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.SystemNotification", - "description": "Discriminant" - }, - "content": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "The text of the system notification" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ - "kind", - "content" + "kind" ] }, - "ConfirmationOption": { + "McpServerReadyState": { "type": "object", - "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "description": "Server is running and serving requests.", "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the option, returned in the confirmed action" - }, - "label": { - "type": "string", - "description": "Human-readable label displayed to the user" - }, "kind": { - "$ref": "#/$defs/ConfirmationOptionKind", - "description": "Whether this option represents an approval or denial" - }, - "group": { - "type": "number", - "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ - "id", - "label", "kind" ] }, - "ToolCallClientContributor": { + "McpServerAuthRequiredState": { "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/ToolCallContributorKind.Client" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, - "clientId": { + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "description": "Human-readable hint, typically from the OAuth `error_description`." } }, "required": [ "kind", - "clientId" + "reason", + "resource" ] }, - "ToolCallMcpContributor": { + "McpServerErrorState": { "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/ToolCallContributorKind.MCP" + "$ref": "#/$defs/McpServerStatus.Error" }, - "customizationId": { - "type": "string", - "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." } }, "required": [ "kind", - "customizationId" + "error" ] }, - "ToolCallBase": { + "McpServerStoppedState": { "type": "object", - "description": "Metadata common to all tool call states.", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ - "toolCallId", - "toolName", - "displayName" + "kind" ] }, - "ToolCallParameterFields": { + "ChatState": { "type": "object", - "description": "Properties available once tool call parameters are fully received.", + "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", "properties": { - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" }, - "toolInput": { + "title": { "type": "string", - "description": "Raw tool input" - } - }, - "required": [ - "invocationMessage" - ] - }, - "ToolCallResult": { - "type": "object", - "description": "Tool execution result details, available after execution completes.", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Chat title" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" }, - "content": { + "activity": { + "type": "string", + "description": "Human-readable description of what the chat is currently doing" + }, + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + }, + "turns": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/Turn" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "description": "Completed turns" }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "activeTurn": { + "$ref": "#/$defs/ActiveTurn", + "description": "Currently in-progress turn" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "steeringMessage": { + "$ref": "#/$defs/PendingMessage", + "description": "Message to inject into the current turn at a convenient point" + }, + "queuedMessages": { + "type": "array", + "items": { + "$ref": "#/$defs/PendingMessage" }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "description": "Messages to send automatically as new turns after the current turn finishes" + }, + "inputRequests": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputRequest" + }, + "description": "Requests for user input that are currently blocking or informing chat progress" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this chat." } }, "required": [ - "success", - "pastTenseMessage" + "resource", + "title", + "status", + "modifiedAt", + "turns" ] }, - "ToolCallStreamingState": { + "ChatSummary": { "type": "object", - "description": "LM is streaming the tool call parameters.", + "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", "properties": { - "toolCallId": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" + }, + "title": { "type": "string", - "description": "Unique tool call identifier" + "description": "Chat title" }, - "toolName": { + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Human-readable description of what the chat is currently doing" }, - "displayName": { + "modifiedAt": { "type": "string", - "description": "Human-readable tool name" + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Streaming" + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" }, - "partialInput": { + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + } + }, + "required": [ + "resource", + "title", + "status", + "modifiedAt" + ] + }, + "PendingMessage": { + "type": "object", + "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", + "properties": { + "id": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Unique identifier for this pending message" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Progress message shown while parameters are streaming" + "message": { + "$ref": "#/$defs/Message", + "description": "The message that will start the next turn" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "status" + "id", + "message" ] }, - "ToolCallPendingConfirmationState": { + "ChatInputOption": { "type": "object", - "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "description": "A choice in a select-style question.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable option identifier; for MCP enum values this is the enum string" }, - "toolName": { + "label": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Display label" }, - "displayName": { + "description": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Optional secondary text" }, - "toolInput": { + "recommended": { + "type": "boolean", + "description": "Whether this option is the recommended/default choice" + } + }, + "required": [ + "id", + "label" + ] + }, + "ChatInputQuestionBase": { + "type": "object", + "properties": { + "id": { "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + "description": "Stable question identifier used as the key in `answers`" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "title": { + "type": "string", + "description": "Short display title" }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "string" - } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "editable": { + "required": { "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ConfirmationOption" - }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "description": "Whether the user must answer this question to accept the request" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status" + "id", + "message" ] }, - "ToolCallRunningState": { + "ChatInputTextQuestion": { "type": "object", - "description": "Tool is actively executing.", + "description": "Text question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "description": "Prompt shown to the user" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Text" }, - "toolInput": { + "format": { "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Running" + "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "min": { + "type": "number", + "description": "Minimum string length" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "max": { + "type": "number", + "description": "Maximum string length" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + "defaultValue": { + "type": "string", + "description": "Default text" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "confirmed" + "id", + "message", + "kind" ] }, - "ToolCallPendingResultConfirmationState": { + "ChatInputNumberQuestion": { "type": "object", - "description": "Tool finished executing, waiting for client to approve the result.", + "description": "Numeric question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" + "description": "Prompt shown to the user" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputQuestionKind.Number" + }, + { + "$ref": "#/$defs/ChatInputQuestionKind.Integer" + } + ] }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "min": { + "type": "number", + "description": "Minimum value" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "max": { + "type": "number", + "description": "Maximum value" }, - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "defaultValue": { + "type": "number", + "description": "Default numeric value" + } + }, + "required": [ + "id", + "message", + "kind" + ] + }, + "ChatInputBooleanQuestion": { + "type": "object", + "description": "Boolean question within a chat input request.", + "properties": { + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "title": { + "type": "string", + "description": "Short display title" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Boolean" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "defaultValue": { + "type": "boolean", + "description": "Default boolean value" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id", + "message", + "kind" ] }, - "ToolCallCompletedState": { + "ChatInputSingleSelectQuestion": { "type": "object", - "description": "Tool completed successfully or with an error.", + "description": "Single-select question within a chat input request.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { + "id": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Stable question identifier used as the key in `answers`" }, - "displayName": { + "title": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Short display title" }, - "toolInput": { + "message": { "type": "string", - "description": "Raw tool input" + "description": "Prompt shown to the user" }, - "success": { + "required": { "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Whether the user must answer this question to accept the request" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.SingleSelect" }, - "content": { + "options": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChatInputOption" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Completed" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "description": "Options the user may select from" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text instead of selecting an option" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id", + "message", + "kind", + "options" ] }, - "ToolCallCancelledState": { + "ChatInputMultiSelectQuestion": { "type": "object", - "description": "Tool call was cancelled before execution.", + "description": "Multi-select question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Prompt shown to the user" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Cancelled" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.MultiSelect" }, - "reason": { - "$ref": "#/$defs/ToolCallCancellationReason", - "description": "Why the tool was cancelled" + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputOption" + }, + "description": "Options the user may select from" }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional message explaining the cancellation" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text in addition to selecting options" }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" + "min": { + "type": "number", + "description": "Minimum selected item count" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "max": { + "type": "number", + "description": "Maximum selected item count" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "reason" + "id", + "message", + "kind", + "options" ] }, - "ToolDefinition": { + "ChatInputRequest": { "type": "object", - "description": "Describes a tool available in a session, provided by either the server or the active client.", + "description": "A live request for user input.\n\nThe server creates or replaces requests with `chat/inputRequested`.\nClients sync drafts with `chat/inputAnswerChanged` and complete requests\nwith `chat/inputCompleted`.", "properties": { - "name": { + "id": { "type": "string", - "description": "Unique tool identifier" + "description": "Stable request identifier" }, - "title": { + "message": { "type": "string", - "description": "Human-readable display name" + "description": "Display message for the request as a whole" }, - "description": { - "type": "string", - "description": "Description of what the tool does" + "url": { + "$ref": "#/$defs/URI", + "description": "URL the user should review or open, for URL-style elicitations" }, - "inputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } + "questions": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputQuestion" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + "description": "Ordered questions to ask the user" }, - "outputSchema": { + "answers": { "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } + "additionalProperties": { + "$ref": "#/$defs/ChatInputAnswer" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." - }, - "annotations": { - "$ref": "#/$defs/ToolAnnotations", - "description": "Behavioral hints about the tool. All properties are advisory." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." + "description": "Current draft or submitted answers, keyed by question ID" } }, "required": [ - "name" + "id" ] }, - "ToolAnnotations": { + "ChatInputTextAnswerValue": { "type": "object", - "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", + "description": "Value captured for one answer.", "properties": { - "title": { - "type": "string", - "description": "Alternate human-readable title" - }, - "readOnlyHint": { - "type": "boolean", - "description": "Tool does not modify its environment (default: false)" - }, - "destructiveHint": { - "type": "boolean", - "description": "Tool may perform destructive updates (default: true)" - }, - "idempotentHint": { - "type": "boolean", - "description": "Repeated calls with the same arguments have no additional effect (default: false)" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Text" }, - "openWorldHint": { - "type": "boolean", - "description": "Tool may interact with external entities (default: true)" + "value": { + "type": "string" } - } + }, + "required": [ + "kind", + "value" + ] }, - "ToolResultTextContent": { + "ChatInputNumberAnswerValue": { "type": "object", - "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Text" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Number" }, - "text": { - "type": "string", - "description": "The text content" + "value": { + "type": "number" } }, "required": [ - "type", - "text" + "kind", + "value" ] }, - "ToolResultEmbeddedResourceContent": { + "ChatInputBooleanAnswerValue": { "type": "object", - "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" - }, - "data": { - "type": "string", - "description": "Base64-encoded data" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Boolean" }, - "contentType": { - "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "value": { + "type": "boolean" } }, "required": [ - "type", - "data", - "contentType" + "kind", + "value" ] }, - "ToolResultResourceContent": { + "ChatInputSelectedAnswerValue": { "type": "object", - "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Selected" }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "value": { + "type": "string" }, - "type": { - "$ref": "#/$defs/ToolResultContentType.Resource" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered instead of selecting an option" } }, "required": [ - "uri", - "type" + "kind", + "value" ] }, - "ToolResultFileEditContent": { + "ChatInputSelectedManyAnswerValue": { "type": "object", - "description": "Describes a file modification performed by a tool.", "properties": { - "before": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.SelectedMany" }, - "after": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." + "value": { + "type": "array", + "items": { + "type": "string" + } }, - "diff": { - "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } + "freeformValues": { + "type": "array", + "items": { + "type": "string" }, - "description": "Optional diff display metadata" - }, - "type": { - "$ref": "#/$defs/ToolResultContentType.FileEdit" + "description": "Free-form text entered in addition to selected options" } }, "required": [ - "type" + "kind", + "value" ] }, - "ToolResultTerminalContent": { + "ChatInputAnswered": { "type": "object", - "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Terminal" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "state": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputAnswerState.Draft" + }, + { + "$ref": "#/$defs/ChatInputAnswerState.Submitted" + } + ], + "description": "Answer state" }, - "title": { - "type": "string", - "description": "Display title for the terminal content" + "value": { + "$ref": "#/$defs/ChatInputAnswerValue", + "description": "Answer value" } }, "required": [ - "type", - "resource", - "title" + "state", + "value" ] }, - "ToolResultSubagentContent": { + "ChatInputSkipped": { "type": "object", - "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Subagent" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Subagent session URI (subscribable for full session state)" - }, - "title": { - "type": "string", - "description": "Display title for the subagent" - }, - "agentName": { - "type": "string", - "description": "Internal agent name" + "state": { + "$ref": "#/$defs/ChatInputAnswerState.Skipped", + "description": "Answer state" }, - "description": { - "type": "string", - "description": "Human-readable description of the subagent's task" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form reason or value captured while skipping, if any" } }, "required": [ - "type", - "resource", - "title" + "state" ] }, - "CustomizationBase": { + "Turn": { "type": "object", - "description": "Fields shared by every customization variant.", + "description": "A completed request/response cycle.", "properties": { "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Turn identifier" }, - "name": { - "type": "string", - "description": "Human-readable name." + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "icons": { + "responseParts": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ResponsePart" }, - "description": "Icons for UI display." + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - } + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" + }, + "state": { + "$ref": "#/$defs/TurnState", + "description": "How the turn ended" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details if state is `'error'`" + } }, "required": [ "id", - "uri", - "name" + "message", + "responseParts", + "usage", + "state" ] }, - "CustomizationLoadingState": { + "ActiveTurn": { "type": "object", - "description": "Container is being loaded by the host.", + "description": "An in-progress turn — the assistant is actively streaming.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loading" + "id": { + "type": "string", + "description": "Turn identifier" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" + }, + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" } }, "required": [ - "kind" + "id", + "message", + "responseParts", + "usage" ] }, - "CustomizationLoadedState": { + "Message": { "type": "object", - "description": "Container loaded successfully.", + "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loaded" + "text": { + "type": "string", + "description": "Message text" + }, + "origin": { + "type": "object", + "properties": { + "kind": { + "type": "string" + } + }, + "required": [ + "kind" + ], + "description": "The origin of the message" + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/$defs/MessageAttachment" + }, + "description": "File/selection attachments" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." } }, "required": [ - "kind" + "text", + "origin" ] }, - "CustomizationDegradedState": { + "MessageAttachmentBase": { "type": "object", - "description": "Container partially loaded but has warnings.", + "description": "Common fields shared by all {@link MessageAttachment} variants.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Degraded" + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "message": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { "type": "string", - "description": "Human-readable description of the warning." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." } }, "required": [ - "kind", - "message" + "label" ] }, - "CustomizationErrorState": { + "SimpleMessageAttachment": { "type": "object", - "description": "Container failed to load.", + "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Error" + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "message": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { "type": "string", - "description": "Human-readable error message." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Simple", + "description": "Discriminant" + }, + "modelRepresentation": { + "type": "string", + "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." } }, "required": [ - "kind", - "message" + "label", + "type" ] }, - "ContainerCustomizationBase": { + "MessageEmbeddedResourceAttachment": { "type": "object", - "description": "Fields shared by container customizations.", + "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", "properties": { - "id": { + "label": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "name": { + "displayKind": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "type": { + "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", + "description": "Discriminant" }, - "clientId": { + "data": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Base64-encoded binary data" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "contentType": { + "type": "string", + "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", - "uri", - "name", - "enabled" + "label", + "type", + "data", + "contentType" ] }, - "PluginCustomization": { + "MessageResourceAttachment": { "type": "object", - "description": "An [Open Plugins](https://open-plugins.com/) plugin.", + "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", "properties": { - "id": { + "label": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "name": { + "displayKind": { "type": "string", - "description": "Human-readable name." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "clientId": { + "contentType": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "description": "Content MIME type" }, "type": { - "$ref": "#/$defs/CustomizationType.Plugin" + "$ref": "#/$defs/MessageAttachmentKind.Resource", + "description": "Discriminant" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", + "label", "uri", - "name", - "enabled", "type" ] }, - "ClientPluginCustomization": { + "MessageAnnotationsAttachment": { "type": "object", - "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", + "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { + "label": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, "range": { "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "clientId": { + "displayKind": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "children": { + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Annotations", + "description": "Discriminant" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + }, + "annotationIds": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "type": "string" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Plugin" - }, - "nonce": { - "type": "string", - "description": "Opaque version token used by the host to detect changes." + "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "label", + "type", + "resource" ] }, - "DirectoryCustomization": { + "MarkdownResponsePart": { "type": "object", - "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "kind": { + "$ref": "#/$defs/ResponsePartKind.Markdown", + "description": "Discriminant" }, - "name": { + "id": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "Part identifier, used by `chat/delta` to target this part for content appends" }, - "clientId": { + "content": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Directory" - }, - "contents": { - "$ref": "#/$defs/ChildCustomizationType", - "description": "Which child customization type this directory holds." - }, - "writable": { - "type": "boolean", - "description": "Whether clients may write into this directory." + "description": "Markdown content" } }, "required": [ + "kind", "id", - "uri", - "name", - "enabled", - "type", - "contents", - "writable" + "content" ] }, - "AgentCustomization": { + "ResourceReponsePart": { "type": "object", - "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", + "description": "A content part that's a reference to large content stored outside the state tree.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, "uri": { "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "Content URI" }, - "type": { - "$ref": "#/$defs/CustomizationType.Agent" + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "description": { + "contentType": { "type": "string", - "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + "description": "Content MIME type" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." + "kind": { + "$ref": "#/$defs/ResponsePartKind.ContentRef", + "description": "Discriminant" } }, "required": [ - "id", "uri", - "name", - "type" + "kind" ] }, - "SkillCustomization": { + "ToolCallResponsePart": { "type": "object", - "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", + "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Skill" - }, - "description": { - "type": "string", - "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + "kind": { + "$ref": "#/$defs/ResponsePartKind.ToolCall", + "description": "Discriminant" }, - "disableModelInvocation": { - "type": "boolean", - "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "Full tool call lifecycle state" } }, "required": [ - "id", - "uri", - "name", - "type" + "kind", + "toolCall" ] }, - "PromptCustomization": { + "ReasoningResponsePart": { "type": "object", - "description": "A prompt contributed by a plugin or directory.", + "description": "Reasoning/thinking content from the model.", "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.Reasoning", + "description": "Discriminant" + }, "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "content": { + "type": "string", + "description": "Accumulated reasoning text" + } + }, + "required": [ + "kind", + "id", + "content" + ] + }, + "SystemNotificationResponsePart": { + "type": "object", + "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.SystemNotification", + "description": "Discriminant" }, - "name": { + "content": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "The text of the system notification" + } + }, + "required": [ + "kind", + "content" + ] + }, + "ConfirmationOption": { + "type": "object", + "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "properties": { + "id": { "type": "string", - "description": "Human-readable name." + "description": "Unique identifier for the option, returned in the confirmed action" + }, + "label": { + "type": "string", + "description": "Human-readable label displayed to the user" + }, + "kind": { + "$ref": "#/$defs/ConfirmationOptionKind", + "description": "Whether this option represents an approval or denial" + }, + "group": { + "type": "number", + "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + } + }, + "required": [ + "id", + "label", + "kind" + ] + }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, + "ToolCallBase": { + "type": "object", + "description": "Metadata common to all tool call states.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName" + ] + }, + "ToolCallParameterFields": { + "type": "object", + "description": "Properties available once tool call parameters are fully received.", + "properties": { + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + } + }, + "required": [ + "invocationMessage" + ] + }, + "ToolCallResult": { + "type": "object", + "description": "Tool execution result details, available after execution completes.", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + } + }, + "required": [ + "success", + "pastTenseMessage" + ] + }, + "ToolCallStreamingState": { + "type": "object", + "description": "LM is streaming the tool call parameters.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Streaming" + }, + "partialInput": { + "type": "string", + "description": "Partial parameters accumulated so far" + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Progress message shown while parameters are streaming" + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "status" + ] + }, + "ToolCallPendingConfirmationState": { + "type": "object", + "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + }, + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "edits": { + "type": "object", + "properties": { + "items": { + "type": "string" + } + }, + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" + }, + "editable": { + "type": "boolean", + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfirmationOption" + }, + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status" + ] + }, + "ToolCallRunningState": { + "type": "object", + "description": "Tool is actively executing.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Running" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "confirmed" + ] + }, + "ToolCallPendingResultConfirmationState": { + "type": "object", + "description": "Tool finished executing, waiting for client to approve the result.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } }, - "description": "Icons for UI display." + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" }, - "type": { - "$ref": "#/$defs/CustomizationType.Prompt" + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" }, - "description": { - "type": "string", - "description": "Short description of what the prompt does." + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "RuleCustomization": { + "ToolCallCompletedState": { "type": "object", - "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", + "description": "Tool completed successfully or with an error.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "$ref": "#/$defs/CustomizationType.Rule" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "description": { + "toolInput": { "type": "string", - "description": "Description of what the rule enforces." + "description": "Raw tool input" }, - "alwaysApply": { + "success": { "type": "boolean", - "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." + "description": "Whether the tool succeeded" }, - "globs": { + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Completed" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "HookCustomization": { + "ToolCallCancelledState": { "type": "object", - "description": "A hook manifest contributed by a plugin or directory.", + "description": "Tool call was cancelled before execution.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "$ref": "#/$defs/CustomizationType.Hook" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Cancelled" + }, + "reason": { + "$ref": "#/$defs/ToolCallCancellationReason", + "description": "Why the tool was cancelled" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional message explaining the cancellation" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "reason" ] }, - "McpServerCustomization": { + "ToolResultTextContent": { "type": "object", - "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", + "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "type": { + "$ref": "#/$defs/ToolResultContentType.Text" }, - "name": { + "text": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, + "description": "The text content" + } + }, + "required": [ + "type", + "text" + ] + }, + "ToolResultEmbeddedResourceContent": { + "type": "object", + "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", + "properties": { "type": { - "$ref": "#/$defs/CustomizationType.McpServer" - }, - "enabled": { - "type": "boolean", - "description": "Whether this MCP server is currently enabled." - }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "Current lifecycle state of the MCP server." + "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" }, - "channel": { - "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "data": { + "type": "string", + "description": "Base64-encoded data" }, - "mcpApp": { - "$ref": "#/$defs/McpServerCustomizationApps", - "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" } }, "required": [ - "id", - "uri", - "name", "type", - "enabled", - "state" + "data", + "contentType" ] }, - "McpServerCustomizationApps": { + "ToolResultResourceContent": { "type": "object", - "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "capabilities": { - "$ref": "#/$defs/AhpMcpUiHostCapabilities", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.Resource" } }, "required": [ - "capabilities" + "uri", + "type" ] }, - "AhpMcpUiHostCapabilities": { + "ToolResultFileEditContent": { "type": "object", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "description": "Describes a file modification performed by a tool.", "properties": { - "serverTools": { + "before": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." }, - "serverResources": { + "after": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `resources/*` methods to the upstream server." - }, - "logging": { - "type": "object", - "additionalProperties": {}, - "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." }, - "sampling": { + "diff": { "type": "object", "properties": { - "tools": { - "type": "string" + "added": { + "type": "number" + }, + "removed": { + "type": "number" } }, - "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." - } - } - }, - "McpServerStartingState": { - "type": "object", - "description": "Server is registered with the host but has not yet started.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Starting" + "description": "Optional diff display metadata" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.FileEdit" } }, "required": [ - "kind" + "type" ] }, - "McpServerReadyState": { + "ToolResultTerminalContent": { "type": "object", - "description": "Server is running and serving requests.", + "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Ready" + "type": { + "$ref": "#/$defs/ToolResultContentType.Terminal" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" + }, + "title": { + "type": "string", + "description": "Display title for the terminal content" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, - "McpServerAuthRequiredState": { + "ToolResultSubagentContent": { "type": "object", - "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.AuthRequired" - }, - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "type": { + "$ref": "#/$defs/ToolResultContentType.Subagent" }, "resource": { - "$ref": "#/$defs/ProtectedResourceMetadata", - "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + "$ref": "#/$defs/URI", + "description": "Subagent session URI (subscribable for full session state)" }, - "requiredScopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + "title": { + "type": "string", + "description": "Display title for the subagent" }, - "description": { + "agentName": { "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." - } - }, - "required": [ - "kind", - "reason", - "resource" - ] - }, - "McpServerErrorState": { - "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Error" + "description": "Internal agent name" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details." - } - }, - "required": [ - "kind", - "error" - ] - }, - "McpServerStoppedState": { - "type": "object", - "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Stopped" + "description": { + "type": "string", + "description": "Human-readable description of the subagent's task" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, "TerminalInfo": { @@ -4913,11 +5027,11 @@ }, "FetchTurnsParams": { "type": "object", - "description": "Fetches historical turns for a session. Used for lazy loading of conversation\nhistory.", + "description": "Fetches historical turns for a chat. Used for lazy loading of conversation\nhistory.", "properties": { "channel": { "$ref": "#/$defs/URI", - "description": "Session URI" + "description": "Chat URI" }, "before": { "type": "string", @@ -4959,7 +5073,7 @@ "properties": { "channel": { "$ref": "#/$defs/URI", - "description": "The session URI the completion is being requested for." + "description": "The chat URI the completion is being requested for." }, "kind": { "$ref": "#/$defs/CompletionItemKind", @@ -5023,6 +5137,71 @@ "items" ] }, + "ChatForkSource": { + "type": "object", + "description": "Identifies a source chat and turn to fork from.", + "properties": { + "chat": { + "$ref": "#/$defs/URI", + "description": "URI of the existing chat to fork from" + }, + "turnId": { + "type": "string", + "description": "Turn ID in the source chat; content up to and including this turn's response is copied" + } + }, + "required": [ + "chat", + "turnId" + ] + }, + "CreateChatParams": { + "type": "object", + "description": "Creates a new chat within a session.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Session URI containing the new chat." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "Chat URI (client-chosen, e.g. `ahp-chat:/`)." + }, + "initialMessage": { + "$ref": "#/$defs/Message", + "description": "Optional initial message for the new chat." + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override." + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override." + }, + "source": { + "$ref": "#/$defs/ChatForkSource", + "description": "Optional source chat and turn to fork from." + } + }, + "required": [ + "channel", + "chat" + ] + }, + "DisposeChatParams": { + "type": "object", + "description": "Disposes a chat and cleans up server-side resources.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + } + }, + "required": [ + "channel" + ] + }, "CreateTerminalParams": { "type": "object", "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index f6c2cc4a..e845235f 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -120,7 +120,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory URI for this session" + "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, "changes": { "$ref": "#/$defs/ChangesSummary", @@ -613,7 +613,7 @@ "properties": { "resource": { "$ref": "#/$defs/URI", - "description": "The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`)" + "description": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" }, "state": { "oneOf": [ @@ -811,24 +811,6 @@ "values" ] }, - "PendingMessage": { - "type": "object", - "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this pending message" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that will start the next turn" - } - }, - "required": [ - "id", - "message" - ] - }, "SessionState": { "type": "object", "description": "Full state for a single session, loaded when a client subscribes to the session's URI.", @@ -856,34 +838,16 @@ "$ref": "#/$defs/SessionActiveClient", "description": "The client currently providing tools and interactive capabilities to this session" }, - "turns": { - "type": "array", - "items": { - "$ref": "#/$defs/Turn" - }, - "description": "Completed turns" - }, - "activeTurn": { - "$ref": "#/$defs/ActiveTurn", - "description": "Currently in-progress turn" - }, - "steeringMessage": { - "$ref": "#/$defs/PendingMessage", - "description": "Message to inject into the current turn at a convenient point" - }, - "queuedMessages": { + "chats": { "type": "array", "items": { - "$ref": "#/$defs/PendingMessage" + "$ref": "#/$defs/ChatSummary" }, - "description": "Messages to send automatically as new turns after the current turn finishes" + "description": "Catalog of chats in this session." }, - "inputRequests": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputRequest" - }, - "description": "Requests for user input that are currently blocking or informing session progress" + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "The chat that receives input when the user addresses the session without\nselecting a specific chat. This is a UI routing hint, not a hierarchy\nmarker — chats remain equal peers at the protocol level. Hosts MAY change\nthis over the session's lifetime." }, "config": { "$ref": "#/$defs/SessionConfigState", @@ -912,7 +876,7 @@ "required": [ "summary", "lifecycle", - "turns" + "chats" ] }, "SessionActiveClient": { @@ -967,6 +931,7 @@ }, "SessionSummary": { "type": "object", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `model` / `agent`: the session-level selection. Per-chat overrides are\n surfaced on individual {@link ChatSummary} entries, not aggregated up.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "resource": { "$ref": "#/$defs/URI", @@ -1010,7 +975,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory URI for this session" + "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, "changes": { "$ref": "#/$defs/ChangesSummary", @@ -1194,2451 +1159,2600 @@ "values" ] }, - "SessionInputOption": { + "ToolDefinition": { "type": "object", - "description": "A choice in a select-style question.", + "description": "Describes a tool available in a session, provided by either the server or the active client.", "properties": { - "id": { + "name": { "type": "string", - "description": "Stable option identifier; for MCP enum values this is the enum string" + "description": "Unique tool identifier" }, - "label": { + "title": { "type": "string", - "description": "Display label" + "description": "Human-readable display name" }, "description": { "type": "string", - "description": "Optional secondary text" + "description": "Description of what the tool does" }, - "recommended": { - "type": "boolean", - "description": "Whether this option is the recommended/default choice" + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + }, + "outputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + }, + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Behavioral hints about the tool. All properties are advisory." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "id", - "label" + "name" ] }, - "SessionInputQuestionBase": { + "ToolAnnotations": { "type": "object", + "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, "title": { "type": "string", - "description": "Short display title" + "description": "Alternate human-readable title" }, - "message": { - "type": "string", - "description": "Prompt shown to the user" + "readOnlyHint": { + "type": "boolean", + "description": "Tool does not modify its environment (default: false)" }, - "required": { + "destructiveHint": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Tool may perform destructive updates (default: true)" + }, + "idempotentHint": { + "type": "boolean", + "description": "Repeated calls with the same arguments have no additional effect (default: false)" + }, + "openWorldHint": { + "type": "boolean", + "description": "Tool may interact with external entities (default: true)" } - }, - "required": [ - "id", - "message" - ] + } }, - "SessionInputTextQuestion": { + "CustomizationBase": { "type": "object", - "description": "Text question within a session input request.", + "description": "Fields shared by every customization variant.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Text" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "format": { + "name": { "type": "string", - "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" - }, - "min": { - "type": "number", - "description": "Minimum string length" + "description": "Human-readable name." }, - "max": { - "type": "number", - "description": "Maximum string length" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "defaultValue": { - "type": "string", - "description": "Default text" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." } }, "required": [ "id", - "message", - "kind" + "uri", + "name" ] }, - "SessionInputNumberQuestion": { + "CustomizationLoadingState": { "type": "object", - "description": "Numeric question within a session input request.", + "description": "Container is being loaded by the host.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputQuestionKind.Number" - }, - { - "$ref": "#/$defs/SessionInputQuestionKind.Integer" - } - ] - }, - "min": { - "type": "number", - "description": "Minimum value" - }, - "max": { - "type": "number", - "description": "Maximum value" - }, - "defaultValue": { - "type": "number", - "description": "Default numeric value" + "$ref": "#/$defs/CustomizationLoadStatus.Loading" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputBooleanQuestion": { + "CustomizationLoadedState": { "type": "object", - "description": "Boolean question within a session input request.", + "description": "Container loaded successfully.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Boolean" - }, - "defaultValue": { - "type": "boolean", - "description": "Default boolean value" + "$ref": "#/$defs/CustomizationLoadStatus.Loaded" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputSingleSelectQuestion": { + "CustomizationDegradedState": { "type": "object", - "description": "Single-select question within a session input request.", + "description": "Container partially loaded but has warnings.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" + "kind": { + "$ref": "#/$defs/CustomizationLoadStatus.Degraded" }, "message": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, + "description": "Human-readable description of the warning." + } + }, + "required": [ + "kind", + "message" + ] + }, + "CustomizationErrorState": { + "type": "object", + "description": "Container failed to load.", + "properties": { "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.SingleSelect" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputOption" - }, - "description": "Options the user may select from" + "$ref": "#/$defs/CustomizationLoadStatus.Error" }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text instead of selecting an option" + "message": { + "type": "string", + "description": "Human-readable error message." } }, "required": [ - "id", - "message", "kind", - "options" + "message" ] }, - "SessionInputMultiSelectQuestion": { + "ContainerCustomizationBase": { "type": "object", - "description": "Multi-select question within a session input request.", + "description": "Fields shared by container customizations.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "title": { - "type": "string", - "description": "Short display title" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "message": { + "name": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.MultiSelect" + "description": "Human-readable name." }, - "options": { + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputOption" + "$ref": "#/$defs/Icon" }, - "description": "Options the user may select from" + "description": "Icons for UI display." }, - "allowFreeformInput": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { "type": "boolean", - "description": "Whether the user may enter text in addition to selecting options" + "description": "Whether this container is currently enabled." }, - "min": { - "type": "number", - "description": "Minimum selected item count" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "max": { - "type": "number", - "description": "Maximum selected item count" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." } }, "required": [ "id", - "message", - "kind", - "options" + "uri", + "name", + "enabled" ] }, - "SessionInputRequest": { + "PluginCustomization": { "type": "object", - "description": "A live request for user input.\n\nThe server creates or replaces requests with `session/inputRequested`.\nClients sync drafts with `session/inputAnswerChanged` and complete requests\nwith `session/inputCompleted`.", + "description": "An [Open Plugins](https://open-plugins.com/) plugin.", "properties": { "id": { "type": "string", - "description": "Stable request identifier" - }, - "message": { - "type": "string", - "description": "Display message for the request as a whole" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "url": { + "uri": { "$ref": "#/$defs/URI", - "description": "URL the user should review or open, for URL-style elicitations" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "questions": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputQuestion" - }, - "description": "Ordered questions to ask the user" - }, - "answers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/SessionInputAnswer" + "$ref": "#/$defs/Icon" }, - "description": "Current draft or submitted answers, keyed by question ID" - } - }, - "required": [ - "id" - ] - }, - "SessionInputTextAnswerValue": { - "type": "object", - "description": "Value captured for one answer.", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Text" + "description": "Icons for UI display." }, - "value": { - "type": "string" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputNumberAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Number" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "value": { - "type": "number" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputBooleanAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Boolean" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "value": { - "type": "boolean" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputSelectedAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Selected" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "value": { - "type": "string" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "freeformValues": { + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered instead of selecting an option" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputSelectedManyAnswerValue": { + "ClientPluginCustomization": { "type": "object", + "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.SelectedMany" + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "value": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "type": "string" - } + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "freeformValues": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered in addition to selected options" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" + }, + "nonce": { + "type": "string", + "description": "Opaque version token used by the host to detect changes." } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputAnswered": { - "type": "object", - "properties": { - "state": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputAnswerState.Draft" - }, - { - "$ref": "#/$defs/SessionInputAnswerState.Submitted" - } - ], - "description": "Answer state" - }, - "value": { - "$ref": "#/$defs/SessionInputAnswerValue", - "description": "Answer value" - } - }, - "required": [ - "state", - "value" - ] - }, - "SessionInputSkipped": { - "type": "object", - "properties": { - "state": { - "$ref": "#/$defs/SessionInputAnswerState.Skipped", - "description": "Answer state" - }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form reason or value captured while skipping, if any" - } - }, - "required": [ - "state" - ] - }, - "Turn": { + "DirectoryCustomization": { "type": "object", - "description": "A completed request/response cycle.", + "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "responseParts": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/Icon" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." + "description": "Icons for UI display." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "state": { - "$ref": "#/$defs/TurnState", - "description": "How the turn ended" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details if state is `'error'`" - } - }, - "required": [ - "id", - "message", - "responseParts", - "usage", - "state" - ] - }, - "ActiveTurn": { - "type": "object", - "description": "An in-progress turn — the assistant is actively streaming.", - "properties": { - "id": { + "clientId": { "type": "string", - "description": "Turn identifier" + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "responseParts": { + "children": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/ChildCustomization" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "type": { + "$ref": "#/$defs/CustomizationType.Directory" + }, + "contents": { + "$ref": "#/$defs/ChildCustomizationType", + "description": "Which child customization type this directory holds." + }, + "writable": { + "type": "boolean", + "description": "Whether clients may write into this directory." } }, "required": [ "id", - "message", - "responseParts", - "usage" + "uri", + "name", + "enabled", + "type", + "contents", + "writable" ] }, - "Message": { + "AgentCustomization": { "type": "object", - "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", "properties": { - "text": { + "id": { "type": "string", - "description": "Message text" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "origin": { - "type": "object", - "properties": { - "kind": { - "type": "string" - } - }, - "required": [ - "kind" - ], - "description": "The origin of the message" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "attachments": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/MessageAttachment" + "$ref": "#/$defs/Icon" }, - "description": "File/selection attachments" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." - } - }, - "required": [ - "text", - "origin" - ] - }, - "MessageAttachmentBase": { - "type": "object", - "description": "Common fields shared by all {@link MessageAttachment} variants.", - "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Icons for UI display." }, "range": { "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "displayKind": { + "type": { + "$ref": "#/$defs/CustomizationType.Agent" + }, + "description": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "label" + "id", + "uri", + "name", + "type" ] }, - "SimpleMessageAttachment": { + "SkillCustomization": { "type": "object", - "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", + "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "type": { - "$ref": "#/$defs/MessageAttachmentKind.Simple", - "description": "Discriminant" + "$ref": "#/$defs/CustomizationType.Skill" }, - "modelRepresentation": { + "description": { "type": "string", - "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." } }, "required": [ - "label", + "id", + "uri", + "name", "type" ] }, - "MessageEmbeddedResourceAttachment": { + "PromptCustomization": { "type": "object", - "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "description": "A prompt contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", - "description": "Discriminant" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "data": { - "type": "string", - "description": "Base64-encoded binary data" + "type": { + "$ref": "#/$defs/CustomizationType.Prompt" }, - "contentType": { + "description": { "type": "string", - "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" - }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + "description": "Short description of what the prompt does." } }, "required": [ - "label", - "type", - "data", - "contentType" + "id", + "uri", + "name", + "type" ] }, - "MessageResourceAttachment": { + "RuleCustomization": { "type": "object", - "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", + "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "type": { + "$ref": "#/$defs/CustomizationType.Rule" }, - "contentType": { + "description": { "type": "string", - "description": "Content MIME type" + "description": "Description of what the rule enforces." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Resource", - "description": "Discriminant" + "alwaysApply": { + "type": "boolean", + "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + "globs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." } }, "required": [ - "label", + "id", "uri", + "name", "type" ] }, - "MessageAnnotationsAttachment": { + "HookCustomization": { "type": "object", - "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", + "description": "A hook manifest contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." - }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Annotations", - "description": "Discriminant" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + "description": "Human-readable name." }, - "annotationIds": { + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." - } - }, - "required": [ - "label", - "type", - "resource" - ] - }, - "MarkdownResponsePart": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.Markdown", - "description": "Discriminant" + "description": "Icons for UI display." }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/delta` to target this part for content appends" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "content": { - "type": "string", - "description": "Markdown content" + "type": { + "$ref": "#/$defs/CustomizationType.Hook" } }, "required": [ - "kind", "id", - "content" + "uri", + "name", + "type" ] }, - "ResourceReponsePart": { + "McpServerCustomization": { "type": "object", - "description": "A content part that's a reference to large content stored outside the state tree.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, "uri": { "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "contentType": { + "name": { "type": "string", - "description": "Content MIME type" + "description": "Human-readable name." }, - "kind": { - "$ref": "#/$defs/ResponsePartKind.ContentRef", - "description": "Discriminant" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "type": { + "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ + "id", "uri", - "kind" + "name", + "type", + "enabled", + "state" ] }, - "ToolCallResponsePart": { + "McpServerCustomizationApps": { "type": "object", - "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.ToolCall", - "description": "Discriminant" - }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "Full tool call lifecycle state" + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." } }, "required": [ - "kind", - "toolCall" + "capabilities" ] }, - "ReasoningResponsePart": { + "AhpMcpUiHostCapabilities": { "type": "object", - "description": "Reasoning/thinking content from the model.", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.Reasoning", - "description": "Discriminant" + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/reasoning` to target this part for content appends" + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." }, - "content": { - "type": "string", - "description": "Accumulated reasoning text" + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." } - }, - "required": [ - "kind", - "id", - "content" - ] + } }, - "SystemNotificationResponsePart": { + "McpServerStartingState": { "type": "object", - "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.SystemNotification", - "description": "Discriminant" - }, - "content": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "The text of the system notification" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ - "kind", - "content" + "kind" ] }, - "ConfirmationOption": { + "McpServerReadyState": { "type": "object", - "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "description": "Server is running and serving requests.", "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the option, returned in the confirmed action" - }, - "label": { - "type": "string", - "description": "Human-readable label displayed to the user" - }, "kind": { - "$ref": "#/$defs/ConfirmationOptionKind", - "description": "Whether this option represents an approval or denial" - }, - "group": { - "type": "number", - "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ - "id", - "label", "kind" ] }, - "ToolCallClientContributor": { + "McpServerAuthRequiredState": { "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/ToolCallContributorKind.Client" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, - "clientId": { + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "description": "Human-readable hint, typically from the OAuth `error_description`." } }, "required": [ "kind", - "clientId" + "reason", + "resource" ] }, - "ToolCallMcpContributor": { + "McpServerErrorState": { "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/ToolCallContributorKind.MCP" + "$ref": "#/$defs/McpServerStatus.Error" }, - "customizationId": { - "type": "string", - "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." } }, "required": [ "kind", - "customizationId" + "error" ] }, - "ToolCallBase": { + "McpServerStoppedState": { "type": "object", - "description": "Metadata common to all tool call states.", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ - "toolCallId", - "toolName", - "displayName" + "kind" ] }, - "ToolCallParameterFields": { + "ChatState": { "type": "object", - "description": "Properties available once tool call parameters are fully received.", + "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", "properties": { - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" }, - "toolInput": { + "title": { "type": "string", - "description": "Raw tool input" - } - }, - "required": [ - "invocationMessage" - ] - }, - "ToolCallResult": { - "type": "object", - "description": "Tool execution result details, available after execution completes.", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Chat title" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" }, - "content": { + "activity": { + "type": "string", + "description": "Human-readable description of what the chat is currently doing" + }, + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + }, + "turns": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/Turn" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "description": "Completed turns" }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "activeTurn": { + "$ref": "#/$defs/ActiveTurn", + "description": "Currently in-progress turn" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "steeringMessage": { + "$ref": "#/$defs/PendingMessage", + "description": "Message to inject into the current turn at a convenient point" + }, + "queuedMessages": { + "type": "array", + "items": { + "$ref": "#/$defs/PendingMessage" }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "description": "Messages to send automatically as new turns after the current turn finishes" + }, + "inputRequests": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputRequest" + }, + "description": "Requests for user input that are currently blocking or informing chat progress" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this chat." } }, "required": [ - "success", - "pastTenseMessage" + "resource", + "title", + "status", + "modifiedAt", + "turns" ] }, - "ToolCallStreamingState": { + "ChatSummary": { "type": "object", - "description": "LM is streaming the tool call parameters.", + "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", "properties": { - "toolCallId": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" + }, + "title": { "type": "string", - "description": "Unique tool call identifier" + "description": "Chat title" }, - "toolName": { + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Human-readable description of what the chat is currently doing" }, - "displayName": { + "modifiedAt": { "type": "string", - "description": "Human-readable tool name" + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Streaming" + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" }, - "partialInput": { + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + } + }, + "required": [ + "resource", + "title", + "status", + "modifiedAt" + ] + }, + "PendingMessage": { + "type": "object", + "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", + "properties": { + "id": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Unique identifier for this pending message" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Progress message shown while parameters are streaming" + "message": { + "$ref": "#/$defs/Message", + "description": "The message that will start the next turn" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "status" + "id", + "message" ] }, - "ToolCallPendingConfirmationState": { + "ChatInputOption": { "type": "object", - "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "description": "A choice in a select-style question.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable option identifier; for MCP enum values this is the enum string" }, - "toolName": { + "label": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Display label" }, - "displayName": { + "description": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Optional secondary text" }, - "toolInput": { + "recommended": { + "type": "boolean", + "description": "Whether this option is the recommended/default choice" + } + }, + "required": [ + "id", + "label" + ] + }, + "ChatInputQuestionBase": { + "type": "object", + "properties": { + "id": { "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + "description": "Stable question identifier used as the key in `answers`" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "title": { + "type": "string", + "description": "Short display title" }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "string" - } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "editable": { + "required": { "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ConfirmationOption" - }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "description": "Whether the user must answer this question to accept the request" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status" + "id", + "message" ] }, - "ToolCallRunningState": { + "ChatInputTextQuestion": { "type": "object", - "description": "Tool is actively executing.", + "description": "Text question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "description": "Prompt shown to the user" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Text" }, - "toolInput": { + "format": { "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Running" + "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "min": { + "type": "number", + "description": "Minimum string length" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "max": { + "type": "number", + "description": "Maximum string length" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + "defaultValue": { + "type": "string", + "description": "Default text" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "confirmed" + "id", + "message", + "kind" ] }, - "ToolCallPendingResultConfirmationState": { + "ChatInputNumberQuestion": { "type": "object", - "description": "Tool finished executing, waiting for client to approve the result.", + "description": "Numeric question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" + "description": "Prompt shown to the user" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputQuestionKind.Number" + }, + { + "$ref": "#/$defs/ChatInputQuestionKind.Integer" + } + ] }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "min": { + "type": "number", + "description": "Minimum value" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "max": { + "type": "number", + "description": "Maximum value" }, - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "defaultValue": { + "type": "number", + "description": "Default numeric value" + } + }, + "required": [ + "id", + "message", + "kind" + ] + }, + "ChatInputBooleanQuestion": { + "type": "object", + "description": "Boolean question within a chat input request.", + "properties": { + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "title": { + "type": "string", + "description": "Short display title" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Boolean" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "defaultValue": { + "type": "boolean", + "description": "Default boolean value" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id", + "message", + "kind" ] }, - "ToolCallCompletedState": { + "ChatInputSingleSelectQuestion": { "type": "object", - "description": "Tool completed successfully or with an error.", + "description": "Single-select question within a chat input request.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { + "id": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Stable question identifier used as the key in `answers`" }, - "displayName": { + "title": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Short display title" }, - "toolInput": { + "message": { "type": "string", - "description": "Raw tool input" + "description": "Prompt shown to the user" }, - "success": { + "required": { "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Whether the user must answer this question to accept the request" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.SingleSelect" }, - "content": { + "options": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChatInputOption" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Completed" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "description": "Options the user may select from" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text instead of selecting an option" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id", + "message", + "kind", + "options" ] }, - "ToolCallCancelledState": { + "ChatInputMultiSelectQuestion": { "type": "object", - "description": "Tool call was cancelled before execution.", + "description": "Multi-select question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Prompt shown to the user" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Cancelled" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.MultiSelect" }, - "reason": { - "$ref": "#/$defs/ToolCallCancellationReason", - "description": "Why the tool was cancelled" + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputOption" + }, + "description": "Options the user may select from" }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional message explaining the cancellation" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text in addition to selecting options" }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" + "min": { + "type": "number", + "description": "Minimum selected item count" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "max": { + "type": "number", + "description": "Maximum selected item count" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "reason" + "id", + "message", + "kind", + "options" ] }, - "ToolDefinition": { + "ChatInputRequest": { "type": "object", - "description": "Describes a tool available in a session, provided by either the server or the active client.", + "description": "A live request for user input.\n\nThe server creates or replaces requests with `chat/inputRequested`.\nClients sync drafts with `chat/inputAnswerChanged` and complete requests\nwith `chat/inputCompleted`.", "properties": { - "name": { + "id": { "type": "string", - "description": "Unique tool identifier" + "description": "Stable request identifier" }, - "title": { + "message": { "type": "string", - "description": "Human-readable display name" + "description": "Display message for the request as a whole" }, - "description": { - "type": "string", - "description": "Description of what the tool does" + "url": { + "$ref": "#/$defs/URI", + "description": "URL the user should review or open, for URL-style elicitations" }, - "inputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } + "questions": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputQuestion" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + "description": "Ordered questions to ask the user" }, - "outputSchema": { + "answers": { "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } + "additionalProperties": { + "$ref": "#/$defs/ChatInputAnswer" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." - }, - "annotations": { - "$ref": "#/$defs/ToolAnnotations", - "description": "Behavioral hints about the tool. All properties are advisory." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." + "description": "Current draft or submitted answers, keyed by question ID" } }, "required": [ - "name" + "id" ] }, - "ToolAnnotations": { + "ChatInputTextAnswerValue": { "type": "object", - "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", + "description": "Value captured for one answer.", "properties": { - "title": { - "type": "string", - "description": "Alternate human-readable title" - }, - "readOnlyHint": { - "type": "boolean", - "description": "Tool does not modify its environment (default: false)" - }, - "destructiveHint": { - "type": "boolean", - "description": "Tool may perform destructive updates (default: true)" - }, - "idempotentHint": { - "type": "boolean", - "description": "Repeated calls with the same arguments have no additional effect (default: false)" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Text" }, - "openWorldHint": { - "type": "boolean", - "description": "Tool may interact with external entities (default: true)" + "value": { + "type": "string" } - } + }, + "required": [ + "kind", + "value" + ] }, - "ToolResultTextContent": { + "ChatInputNumberAnswerValue": { "type": "object", - "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Text" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Number" }, - "text": { - "type": "string", - "description": "The text content" + "value": { + "type": "number" } }, "required": [ - "type", - "text" + "kind", + "value" ] }, - "ToolResultEmbeddedResourceContent": { + "ChatInputBooleanAnswerValue": { "type": "object", - "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" - }, - "data": { - "type": "string", - "description": "Base64-encoded data" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Boolean" }, - "contentType": { - "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "value": { + "type": "boolean" } }, "required": [ - "type", - "data", - "contentType" + "kind", + "value" ] }, - "ToolResultResourceContent": { + "ChatInputSelectedAnswerValue": { "type": "object", - "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Selected" }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "value": { + "type": "string" }, - "type": { - "$ref": "#/$defs/ToolResultContentType.Resource" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered instead of selecting an option" } }, "required": [ - "uri", - "type" + "kind", + "value" ] }, - "ToolResultFileEditContent": { + "ChatInputSelectedManyAnswerValue": { "type": "object", - "description": "Describes a file modification performed by a tool.", "properties": { - "before": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.SelectedMany" }, - "after": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." + "value": { + "type": "array", + "items": { + "type": "string" + } }, - "diff": { - "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } + "freeformValues": { + "type": "array", + "items": { + "type": "string" }, - "description": "Optional diff display metadata" - }, - "type": { - "$ref": "#/$defs/ToolResultContentType.FileEdit" + "description": "Free-form text entered in addition to selected options" } }, "required": [ - "type" + "kind", + "value" ] }, - "ToolResultTerminalContent": { + "ChatInputAnswered": { "type": "object", - "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Terminal" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "state": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputAnswerState.Draft" + }, + { + "$ref": "#/$defs/ChatInputAnswerState.Submitted" + } + ], + "description": "Answer state" }, - "title": { - "type": "string", - "description": "Display title for the terminal content" + "value": { + "$ref": "#/$defs/ChatInputAnswerValue", + "description": "Answer value" } }, "required": [ - "type", - "resource", - "title" + "state", + "value" ] }, - "ToolResultSubagentContent": { + "ChatInputSkipped": { "type": "object", - "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Subagent" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Subagent session URI (subscribable for full session state)" - }, - "title": { - "type": "string", - "description": "Display title for the subagent" - }, - "agentName": { - "type": "string", - "description": "Internal agent name" + "state": { + "$ref": "#/$defs/ChatInputAnswerState.Skipped", + "description": "Answer state" }, - "description": { - "type": "string", - "description": "Human-readable description of the subagent's task" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form reason or value captured while skipping, if any" } }, "required": [ - "type", - "resource", - "title" + "state" ] }, - "CustomizationBase": { + "Turn": { "type": "object", - "description": "Fields shared by every customization variant.", + "description": "A completed request/response cycle.", "properties": { "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Turn identifier" }, - "name": { - "type": "string", - "description": "Human-readable name." + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "icons": { + "responseParts": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ResponsePart" }, - "description": "Icons for UI display." + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - } + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" + }, + "state": { + "$ref": "#/$defs/TurnState", + "description": "How the turn ended" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details if state is `'error'`" + } }, "required": [ "id", - "uri", - "name" + "message", + "responseParts", + "usage", + "state" ] }, - "CustomizationLoadingState": { + "ActiveTurn": { "type": "object", - "description": "Container is being loaded by the host.", + "description": "An in-progress turn — the assistant is actively streaming.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loading" + "id": { + "type": "string", + "description": "Turn identifier" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" + }, + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" } }, "required": [ - "kind" + "id", + "message", + "responseParts", + "usage" ] }, - "CustomizationLoadedState": { + "Message": { "type": "object", - "description": "Container loaded successfully.", + "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loaded" + "text": { + "type": "string", + "description": "Message text" + }, + "origin": { + "type": "object", + "properties": { + "kind": { + "type": "string" + } + }, + "required": [ + "kind" + ], + "description": "The origin of the message" + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/$defs/MessageAttachment" + }, + "description": "File/selection attachments" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." } }, "required": [ - "kind" + "text", + "origin" ] }, - "CustomizationDegradedState": { + "MessageAttachmentBase": { "type": "object", - "description": "Container partially loaded but has warnings.", + "description": "Common fields shared by all {@link MessageAttachment} variants.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Degraded" + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "message": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { "type": "string", - "description": "Human-readable description of the warning." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." } }, "required": [ - "kind", - "message" + "label" ] }, - "CustomizationErrorState": { + "SimpleMessageAttachment": { "type": "object", - "description": "Container failed to load.", + "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Error" + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "message": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { "type": "string", - "description": "Human-readable error message." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Simple", + "description": "Discriminant" + }, + "modelRepresentation": { + "type": "string", + "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." } }, "required": [ - "kind", - "message" + "label", + "type" ] }, - "ContainerCustomizationBase": { + "MessageEmbeddedResourceAttachment": { "type": "object", - "description": "Fields shared by container customizations.", + "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", "properties": { - "id": { + "label": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "name": { + "displayKind": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "type": { + "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", + "description": "Discriminant" }, - "clientId": { + "data": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Base64-encoded binary data" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "contentType": { + "type": "string", + "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", - "uri", - "name", - "enabled" + "label", + "type", + "data", + "contentType" ] }, - "PluginCustomization": { + "MessageResourceAttachment": { "type": "object", - "description": "An [Open Plugins](https://open-plugins.com/) plugin.", + "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", "properties": { - "id": { + "label": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "name": { + "displayKind": { "type": "string", - "description": "Human-readable name." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "clientId": { + "contentType": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "description": "Content MIME type" }, "type": { - "$ref": "#/$defs/CustomizationType.Plugin" + "$ref": "#/$defs/MessageAttachmentKind.Resource", + "description": "Discriminant" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", + "label", "uri", - "name", - "enabled", "type" ] }, - "ClientPluginCustomization": { + "MessageAnnotationsAttachment": { "type": "object", - "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", + "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { + "label": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, "range": { "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "clientId": { + "displayKind": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "children": { + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Annotations", + "description": "Discriminant" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + }, + "annotationIds": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "type": "string" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Plugin" - }, - "nonce": { - "type": "string", - "description": "Opaque version token used by the host to detect changes." + "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "label", + "type", + "resource" ] }, - "DirectoryCustomization": { + "MarkdownResponsePart": { "type": "object", - "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "kind": { + "$ref": "#/$defs/ResponsePartKind.Markdown", + "description": "Discriminant" }, - "name": { + "id": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "Part identifier, used by `chat/delta` to target this part for content appends" }, - "clientId": { + "content": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Directory" - }, - "contents": { - "$ref": "#/$defs/ChildCustomizationType", - "description": "Which child customization type this directory holds." - }, - "writable": { - "type": "boolean", - "description": "Whether clients may write into this directory." + "description": "Markdown content" } }, "required": [ + "kind", "id", - "uri", - "name", - "enabled", - "type", - "contents", - "writable" + "content" ] }, - "AgentCustomization": { + "ResourceReponsePart": { "type": "object", - "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", + "description": "A content part that's a reference to large content stored outside the state tree.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, "uri": { "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "Content URI" }, - "type": { - "$ref": "#/$defs/CustomizationType.Agent" + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "description": { + "contentType": { "type": "string", - "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + "description": "Content MIME type" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." + "kind": { + "$ref": "#/$defs/ResponsePartKind.ContentRef", + "description": "Discriminant" } }, "required": [ - "id", "uri", - "name", - "type" + "kind" ] }, - "SkillCustomization": { + "ToolCallResponsePart": { "type": "object", - "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", + "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Skill" - }, - "description": { - "type": "string", - "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + "kind": { + "$ref": "#/$defs/ResponsePartKind.ToolCall", + "description": "Discriminant" }, - "disableModelInvocation": { - "type": "boolean", - "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "Full tool call lifecycle state" } }, "required": [ - "id", - "uri", - "name", - "type" + "kind", + "toolCall" ] }, - "PromptCustomization": { + "ReasoningResponsePart": { "type": "object", - "description": "A prompt contributed by a plugin or directory.", + "description": "Reasoning/thinking content from the model.", "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.Reasoning", + "description": "Discriminant" + }, "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "content": { + "type": "string", + "description": "Accumulated reasoning text" + } + }, + "required": [ + "kind", + "id", + "content" + ] + }, + "SystemNotificationResponsePart": { + "type": "object", + "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.SystemNotification", + "description": "Discriminant" + }, + "content": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "The text of the system notification" + } + }, + "required": [ + "kind", + "content" + ] + }, + "ConfirmationOption": { + "type": "object", + "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the option, returned in the confirmed action" + }, + "label": { + "type": "string", + "description": "Human-readable label displayed to the user" + }, + "kind": { + "$ref": "#/$defs/ConfirmationOptionKind", + "description": "Whether this option represents an approval or denial" + }, + "group": { + "type": "number", + "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + } + }, + "required": [ + "id", + "label", + "kind" + ] + }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, + "ToolCallBase": { + "type": "object", + "description": "Metadata common to all tool call states.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName" + ] + }, + "ToolCallParameterFields": { + "type": "object", + "description": "Properties available once tool call parameters are fully received.", + "properties": { + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + } + }, + "required": [ + "invocationMessage" + ] + }, + "ToolCallResult": { + "type": "object", + "description": "Tool execution result details, available after execution completes.", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + } + }, + "required": [ + "success", + "pastTenseMessage" + ] + }, + "ToolCallStreamingState": { + "type": "object", + "description": "LM is streaming the tool call parameters.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Streaming" + }, + "partialInput": { + "type": "string", + "description": "Partial parameters accumulated so far" + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Progress message shown while parameters are streaming" + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "status" + ] + }, + "ToolCallPendingConfirmationState": { + "type": "object", + "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + }, + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "edits": { + "type": "object", + "properties": { + "items": { + "type": "string" + } + }, + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" + }, + "editable": { + "type": "boolean", + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfirmationOption" + }, + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status" + ] + }, + "ToolCallRunningState": { + "type": "object", + "description": "Tool is actively executing.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Running" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "confirmed" + ] + }, + "ToolCallPendingResultConfirmationState": { + "type": "object", + "description": "Tool finished executing, waiting for client to approve the result.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "name": { + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { "type": "string", - "description": "Human-readable name." + "description": "Raw tool input" }, - "icons": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Icons for UI display." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "type": { - "$ref": "#/$defs/CustomizationType.Prompt" + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "description": { - "type": "string", - "description": "Short description of what the prompt does." + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "RuleCustomization": { + "ToolCallCompletedState": { "type": "object", - "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", + "description": "Tool completed successfully or with an error.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "$ref": "#/$defs/CustomizationType.Rule" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "description": { + "toolInput": { "type": "string", - "description": "Description of what the rule enforces." + "description": "Raw tool input" }, - "alwaysApply": { + "success": { "type": "boolean", - "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." + "description": "Whether the tool succeeded" }, - "globs": { + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Completed" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" + ] + }, + "ToolCallCancelledState": { + "type": "object", + "description": "Tool call was cancelled before execution.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Cancelled" + }, + "reason": { + "$ref": "#/$defs/ToolCallCancellationReason", + "description": "Why the tool was cancelled" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional message explaining the cancellation" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "reason" + ] + }, + "ToolResultTextContent": { + "type": "object", + "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", + "properties": { + "type": { + "$ref": "#/$defs/ToolResultContentType.Text" + }, + "text": { + "type": "string", + "description": "The text content" + } + }, + "required": [ + "type", + "text" ] }, - "HookCustomization": { + "ToolResultEmbeddedResourceContent": { "type": "object", - "description": "A hook manifest contributed by a plugin or directory.", + "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "type": { + "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" }, - "name": { + "data": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "Base64-encoded data" }, - "type": { - "$ref": "#/$defs/CustomizationType.Hook" + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" } }, "required": [ - "id", - "uri", - "name", - "type" + "type", + "data", + "contentType" ] }, - "McpServerCustomization": { + "ToolResultResourceContent": { "type": "object", - "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", + "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, "uri": { "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." + "description": "Content URI" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "contentType": { + "type": "string", + "description": "Content MIME type" }, "type": { - "$ref": "#/$defs/CustomizationType.McpServer" - }, - "enabled": { - "type": "boolean", - "description": "Whether this MCP server is currently enabled." - }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "Current lifecycle state of the MCP server." - }, - "channel": { - "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." - }, - "mcpApp": { - "$ref": "#/$defs/McpServerCustomizationApps", - "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + "$ref": "#/$defs/ToolResultContentType.Resource" } }, "required": [ - "id", "uri", - "name", - "type", - "enabled", - "state" - ] - }, - "McpServerCustomizationApps": { - "type": "object", - "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", - "properties": { - "capabilities": { - "$ref": "#/$defs/AhpMcpUiHostCapabilities", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." - } - }, - "required": [ - "capabilities" + "type" ] }, - "AhpMcpUiHostCapabilities": { + "ToolResultFileEditContent": { "type": "object", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "description": "Describes a file modification performed by a tool.", "properties": { - "serverTools": { + "before": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." }, - "serverResources": { + "after": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `resources/*` methods to the upstream server." - }, - "logging": { - "type": "object", - "additionalProperties": {}, - "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." }, - "sampling": { + "diff": { "type": "object", "properties": { - "tools": { - "type": "string" + "added": { + "type": "number" + }, + "removed": { + "type": "number" } }, - "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." - } - } - }, - "McpServerStartingState": { - "type": "object", - "description": "Server is registered with the host but has not yet started.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Starting" + "description": "Optional diff display metadata" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.FileEdit" } }, "required": [ - "kind" + "type" ] }, - "McpServerReadyState": { + "ToolResultTerminalContent": { "type": "object", - "description": "Server is running and serving requests.", + "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Ready" + "type": { + "$ref": "#/$defs/ToolResultContentType.Terminal" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" + }, + "title": { + "type": "string", + "description": "Display title for the terminal content" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, - "McpServerAuthRequiredState": { + "ToolResultSubagentContent": { "type": "object", - "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.AuthRequired" - }, - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "type": { + "$ref": "#/$defs/ToolResultContentType.Subagent" }, "resource": { - "$ref": "#/$defs/ProtectedResourceMetadata", - "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + "$ref": "#/$defs/URI", + "description": "Subagent session URI (subscribable for full session state)" }, - "requiredScopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + "title": { + "type": "string", + "description": "Display title for the subagent" }, - "description": { + "agentName": { "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." - } - }, - "required": [ - "kind", - "reason", - "resource" - ] - }, - "McpServerErrorState": { - "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Error" + "description": "Internal agent name" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details." - } - }, - "required": [ - "kind", - "error" - ] - }, - "McpServerStoppedState": { - "type": "object", - "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Stopped" + "description": { + "type": "string", + "description": "Human-readable description of the subagent's task" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, "TerminalInfo": { diff --git a/schema/state.schema.json b/schema/state.schema.json index c18dd94a..27057acb 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -395,7 +395,7 @@ "properties": { "resource": { "$ref": "#/$defs/URI", - "description": "The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`)" + "description": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" }, "state": { "oneOf": [ @@ -593,24 +593,6 @@ "values" ] }, - "PendingMessage": { - "type": "object", - "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this pending message" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that will start the next turn" - } - }, - "required": [ - "id", - "message" - ] - }, "SessionState": { "type": "object", "description": "Full state for a single session, loaded when a client subscribes to the session's URI.", @@ -638,34 +620,16 @@ "$ref": "#/$defs/SessionActiveClient", "description": "The client currently providing tools and interactive capabilities to this session" }, - "turns": { - "type": "array", - "items": { - "$ref": "#/$defs/Turn" - }, - "description": "Completed turns" - }, - "activeTurn": { - "$ref": "#/$defs/ActiveTurn", - "description": "Currently in-progress turn" - }, - "steeringMessage": { - "$ref": "#/$defs/PendingMessage", - "description": "Message to inject into the current turn at a convenient point" - }, - "queuedMessages": { + "chats": { "type": "array", "items": { - "$ref": "#/$defs/PendingMessage" + "$ref": "#/$defs/ChatSummary" }, - "description": "Messages to send automatically as new turns after the current turn finishes" + "description": "Catalog of chats in this session." }, - "inputRequests": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputRequest" - }, - "description": "Requests for user input that are currently blocking or informing session progress" + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "The chat that receives input when the user addresses the session without\nselecting a specific chat. This is a UI routing hint, not a hierarchy\nmarker — chats remain equal peers at the protocol level. Hosts MAY change\nthis over the session's lifetime." }, "config": { "$ref": "#/$defs/SessionConfigState", @@ -694,7 +658,7 @@ "required": [ "summary", "lifecycle", - "turns" + "chats" ] }, "SessionActiveClient": { @@ -749,6 +713,7 @@ }, "SessionSummary": { "type": "object", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `model` / `agent`: the session-level selection. Per-chat overrides are\n surfaced on individual {@link ChatSummary} entries, not aggregated up.\n- `workingDirectory`: the session-level **default**. Individual chats MAY\n override via {@link ChatSummary.workingDirectory}; aggregating these up\n is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "resource": { "$ref": "#/$defs/URI", @@ -792,7 +757,7 @@ }, "workingDirectory": { "$ref": "#/$defs/URI", - "description": "The working directory URI for this session" + "description": "The default working directory URI for this session. Individual chats\nMAY override via {@link ChatSummary.workingDirectory | their own\n`workingDirectory`}; this field acts as the fallback for any chat that\ndoes not." }, "changes": { "$ref": "#/$defs/ChangesSummary", @@ -976,2451 +941,2600 @@ "values" ] }, - "SessionInputOption": { + "ToolDefinition": { "type": "object", - "description": "A choice in a select-style question.", + "description": "Describes a tool available in a session, provided by either the server or the active client.", "properties": { - "id": { + "name": { "type": "string", - "description": "Stable option identifier; for MCP enum values this is the enum string" + "description": "Unique tool identifier" }, - "label": { + "title": { "type": "string", - "description": "Display label" + "description": "Human-readable display name" }, "description": { "type": "string", - "description": "Optional secondary text" + "description": "Description of what the tool does" }, - "recommended": { - "type": "boolean", - "description": "Whether this option is the recommended/default choice" + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + }, + "outputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "required": { + "type": "string" + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + }, + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Behavioral hints about the tool. All properties are advisory." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "id", - "label" + "name" ] }, - "SessionInputQuestionBase": { + "ToolAnnotations": { "type": "object", + "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, "title": { "type": "string", - "description": "Short display title" + "description": "Alternate human-readable title" }, - "message": { - "type": "string", - "description": "Prompt shown to the user" + "readOnlyHint": { + "type": "boolean", + "description": "Tool does not modify its environment (default: false)" }, - "required": { + "destructiveHint": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Tool may perform destructive updates (default: true)" + }, + "idempotentHint": { + "type": "boolean", + "description": "Repeated calls with the same arguments have no additional effect (default: false)" + }, + "openWorldHint": { + "type": "boolean", + "description": "Tool may interact with external entities (default: true)" } - }, - "required": [ - "id", - "message" - ] + } }, - "SessionInputTextQuestion": { + "CustomizationBase": { "type": "object", - "description": "Text question within a session input request.", + "description": "Fields shared by every customization variant.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Text" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "format": { + "name": { "type": "string", - "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" - }, - "min": { - "type": "number", - "description": "Minimum string length" + "description": "Human-readable name." }, - "max": { - "type": "number", - "description": "Maximum string length" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "defaultValue": { - "type": "string", - "description": "Default text" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." } }, "required": [ "id", - "message", - "kind" + "uri", + "name" ] }, - "SessionInputNumberQuestion": { + "CustomizationLoadingState": { "type": "object", - "description": "Numeric question within a session input request.", + "description": "Container is being loaded by the host.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputQuestionKind.Number" - }, - { - "$ref": "#/$defs/SessionInputQuestionKind.Integer" - } - ] - }, - "min": { - "type": "number", - "description": "Minimum value" - }, - "max": { - "type": "number", - "description": "Maximum value" - }, - "defaultValue": { - "type": "number", - "description": "Default numeric value" + "$ref": "#/$defs/CustomizationLoadStatus.Loading" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputBooleanQuestion": { + "CustomizationLoadedState": { "type": "object", - "description": "Boolean question within a session input request.", + "description": "Container loaded successfully.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.Boolean" - }, - "defaultValue": { - "type": "boolean", - "description": "Default boolean value" + "$ref": "#/$defs/CustomizationLoadStatus.Loaded" } }, "required": [ - "id", - "message", "kind" ] }, - "SessionInputSingleSelectQuestion": { + "CustomizationDegradedState": { "type": "object", - "description": "Single-select question within a session input request.", + "description": "Container partially loaded but has warnings.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" + "kind": { + "$ref": "#/$defs/CustomizationLoadStatus.Degraded" }, "message": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, + "description": "Human-readable description of the warning." + } + }, + "required": [ + "kind", + "message" + ] + }, + "CustomizationErrorState": { + "type": "object", + "description": "Container failed to load.", + "properties": { "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.SingleSelect" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputOption" - }, - "description": "Options the user may select from" + "$ref": "#/$defs/CustomizationLoadStatus.Error" }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text instead of selecting an option" + "message": { + "type": "string", + "description": "Human-readable error message." } }, "required": [ - "id", - "message", "kind", - "options" + "message" ] }, - "SessionInputMultiSelectQuestion": { + "ContainerCustomizationBase": { "type": "object", - "description": "Multi-select question within a session input request.", + "description": "Fields shared by container customizations.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "title": { - "type": "string", - "description": "Short display title" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "message": { + "name": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, - "kind": { - "$ref": "#/$defs/SessionInputQuestionKind.MultiSelect" + "description": "Human-readable name." }, - "options": { + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputOption" + "$ref": "#/$defs/Icon" }, - "description": "Options the user may select from" + "description": "Icons for UI display." }, - "allowFreeformInput": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { "type": "boolean", - "description": "Whether the user may enter text in addition to selecting options" + "description": "Whether this container is currently enabled." }, - "min": { - "type": "number", - "description": "Minimum selected item count" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "max": { - "type": "number", - "description": "Maximum selected item count" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." } }, "required": [ "id", - "message", - "kind", - "options" + "uri", + "name", + "enabled" ] }, - "SessionInputRequest": { + "PluginCustomization": { "type": "object", - "description": "A live request for user input.\n\nThe server creates or replaces requests with `session/inputRequested`.\nClients sync drafts with `session/inputAnswerChanged` and complete requests\nwith `session/inputCompleted`.", + "description": "An [Open Plugins](https://open-plugins.com/) plugin.", "properties": { "id": { "type": "string", - "description": "Stable request identifier" - }, - "message": { - "type": "string", - "description": "Display message for the request as a whole" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "url": { + "uri": { "$ref": "#/$defs/URI", - "description": "URL the user should review or open, for URL-style elicitations" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "questions": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/SessionInputQuestion" - }, - "description": "Ordered questions to ask the user" - }, - "answers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/SessionInputAnswer" + "$ref": "#/$defs/Icon" }, - "description": "Current draft or submitted answers, keyed by question ID" - } - }, - "required": [ - "id" - ] - }, - "SessionInputTextAnswerValue": { - "type": "object", - "description": "Value captured for one answer.", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Text" + "description": "Icons for UI display." }, - "value": { - "type": "string" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputNumberAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Number" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "value": { - "type": "number" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputBooleanAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Boolean" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "value": { - "type": "boolean" - } - }, - "required": [ - "kind", - "value" - ] - }, - "SessionInputSelectedAnswerValue": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.Selected" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "value": { - "type": "string" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "freeformValues": { + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered instead of selecting an option" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputSelectedManyAnswerValue": { + "ClientPluginCustomization": { "type": "object", + "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", "properties": { - "kind": { - "$ref": "#/$defs/SessionInputAnswerValueKind.SelectedMany" + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "value": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "type": "string" - } + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "freeformValues": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Free-form text entered in addition to selected options" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "$ref": "#/$defs/CustomizationType.Plugin" + }, + "nonce": { + "type": "string", + "description": "Opaque version token used by the host to detect changes." } }, "required": [ - "kind", - "value" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionInputAnswered": { - "type": "object", - "properties": { - "state": { - "oneOf": [ - { - "$ref": "#/$defs/SessionInputAnswerState.Draft" - }, - { - "$ref": "#/$defs/SessionInputAnswerState.Submitted" - } - ], - "description": "Answer state" - }, - "value": { - "$ref": "#/$defs/SessionInputAnswerValue", - "description": "Answer value" - } - }, - "required": [ - "state", - "value" - ] - }, - "SessionInputSkipped": { - "type": "object", - "properties": { - "state": { - "$ref": "#/$defs/SessionInputAnswerState.Skipped", - "description": "Answer state" - }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form reason or value captured while skipping, if any" - } - }, - "required": [ - "state" - ] - }, - "Turn": { + "DirectoryCustomization": { "type": "object", - "description": "A completed request/response cycle.", + "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "responseParts": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/Icon" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." + "description": "Icons for UI display." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "state": { - "$ref": "#/$defs/TurnState", - "description": "How the turn ended" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details if state is `'error'`" - } - }, - "required": [ - "id", - "message", - "responseParts", - "usage", - "state" - ] - }, - "ActiveTurn": { - "type": "object", - "description": "An in-progress turn — the assistant is actively streaming.", - "properties": { - "id": { + "clientId": { "type": "string", - "description": "Turn identifier" + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "responseParts": { + "children": { "type": "array", "items": { - "$ref": "#/$defs/ResponsePart" + "$ref": "#/$defs/ChildCustomization" }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "type": { + "$ref": "#/$defs/CustomizationType.Directory" + }, + "contents": { + "$ref": "#/$defs/ChildCustomizationType", + "description": "Which child customization type this directory holds." + }, + "writable": { + "type": "boolean", + "description": "Whether clients may write into this directory." } }, "required": [ "id", - "message", - "responseParts", - "usage" + "uri", + "name", + "enabled", + "type", + "contents", + "writable" ] }, - "Message": { + "AgentCustomization": { "type": "object", - "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", "properties": { - "text": { + "id": { "type": "string", - "description": "Message text" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "origin": { - "type": "object", - "properties": { - "kind": { - "type": "string" - } - }, - "required": [ - "kind" - ], - "description": "The origin of the message" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "attachments": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "$ref": "#/$defs/MessageAttachment" + "$ref": "#/$defs/Icon" }, - "description": "File/selection attachments" - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." - } - }, - "required": [ - "text", - "origin" - ] - }, - "MessageAttachmentBase": { - "type": "object", - "description": "Common fields shared by all {@link MessageAttachment} variants.", - "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Icons for UI display." }, "range": { "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "displayKind": { + "type": { + "$ref": "#/$defs/CustomizationType.Agent" + }, + "description": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "label" + "id", + "uri", + "name", + "type" ] }, - "SimpleMessageAttachment": { + "SkillCustomization": { "type": "object", - "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", + "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "type": { - "$ref": "#/$defs/MessageAttachmentKind.Simple", - "description": "Discriminant" + "$ref": "#/$defs/CustomizationType.Skill" }, - "modelRepresentation": { + "description": { "type": "string", - "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." } }, "required": [ - "label", + "id", + "uri", + "name", "type" ] }, - "MessageEmbeddedResourceAttachment": { + "PromptCustomization": { "type": "object", - "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "description": "A prompt contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", - "description": "Discriminant" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "data": { - "type": "string", - "description": "Base64-encoded binary data" + "type": { + "$ref": "#/$defs/CustomizationType.Prompt" }, - "contentType": { + "description": { "type": "string", - "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" - }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + "description": "Short description of what the prompt does." } }, "required": [ - "label", - "type", - "data", - "contentType" + "id", + "uri", + "name", + "type" ] }, - "MessageResourceAttachment": { + "RuleCustomization": { "type": "object", - "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", + "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable name." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "type": { + "$ref": "#/$defs/CustomizationType.Rule" }, - "contentType": { + "description": { "type": "string", - "description": "Content MIME type" + "description": "Description of what the rule enforces." }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Resource", - "description": "Discriminant" + "alwaysApply": { + "type": "boolean", + "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + "globs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." } }, "required": [ - "label", + "id", "uri", + "name", "type" ] }, - "MessageAnnotationsAttachment": { + "HookCustomization": { "type": "object", - "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", + "description": "A hook manifest contributed by a plugin or directory.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "displayKind": { + "name": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." - }, - "type": { - "$ref": "#/$defs/MessageAttachmentKind.Annotations", - "description": "Discriminant" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + "description": "Human-readable name." }, - "annotationIds": { + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." - } - }, - "required": [ - "label", - "type", - "resource" - ] - }, - "MarkdownResponsePart": { - "type": "object", - "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.Markdown", - "description": "Discriminant" + "description": "Icons for UI display." }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/delta` to target this part for content appends" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "content": { - "type": "string", - "description": "Markdown content" + "type": { + "$ref": "#/$defs/CustomizationType.Hook" } }, "required": [ - "kind", "id", - "content" + "uri", + "name", + "type" ] }, - "ResourceReponsePart": { + "McpServerCustomization": { "type": "object", - "description": "A content part that's a reference to large content stored outside the state tree.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, "uri": { "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "contentType": { + "name": { "type": "string", - "description": "Content MIME type" + "description": "Human-readable name." }, - "kind": { - "$ref": "#/$defs/ResponsePartKind.ContentRef", - "description": "Discriminant" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "type": { + "$ref": "#/$defs/CustomizationType.McpServer" + }, + "enabled": { + "type": "boolean", + "description": "Whether this MCP server is currently enabled." + }, + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ + "id", "uri", - "kind" + "name", + "type", + "enabled", + "state" ] }, - "ToolCallResponsePart": { + "McpServerCustomizationApps": { "type": "object", - "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.ToolCall", - "description": "Discriminant" - }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "Full tool call lifecycle state" + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." } }, "required": [ - "kind", - "toolCall" + "capabilities" ] }, - "ReasoningResponsePart": { + "AhpMcpUiHostCapabilities": { "type": "object", - "description": "Reasoning/thinking content from the model.", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", "properties": { - "kind": { - "$ref": "#/$defs/ResponsePartKind.Reasoning", - "description": "Discriminant" + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." }, - "id": { - "type": "string", - "description": "Part identifier, used by `session/reasoning` to target this part for content appends" + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." }, - "content": { - "type": "string", - "description": "Accumulated reasoning text" + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "string" + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." } - }, - "required": [ - "kind", - "id", - "content" - ] + } }, - "SystemNotificationResponsePart": { + "McpServerStartingState": { "type": "object", - "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "description": "Server is registered with the host but has not yet started.", "properties": { "kind": { - "$ref": "#/$defs/ResponsePartKind.SystemNotification", - "description": "Discriminant" - }, - "content": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "The text of the system notification" + "$ref": "#/$defs/McpServerStatus.Starting" } }, "required": [ - "kind", - "content" + "kind" ] }, - "ConfirmationOption": { + "McpServerReadyState": { "type": "object", - "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "description": "Server is running and serving requests.", "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the option, returned in the confirmed action" - }, - "label": { - "type": "string", - "description": "Human-readable label displayed to the user" - }, "kind": { - "$ref": "#/$defs/ConfirmationOptionKind", - "description": "Whether this option represents an approval or denial" - }, - "group": { - "type": "number", - "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + "$ref": "#/$defs/McpServerStatus.Ready" } }, "required": [ - "id", - "label", "kind" ] }, - "ToolCallClientContributor": { + "McpServerAuthRequiredState": { "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { "kind": { - "$ref": "#/$defs/ToolCallContributorKind.Client" + "$ref": "#/$defs/McpServerStatus.AuthRequired" }, - "clientId": { + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `session/toolCallComplete` with the result." + "description": "Human-readable hint, typically from the OAuth `error_description`." } }, "required": [ "kind", - "clientId" + "reason", + "resource" ] }, - "ToolCallMcpContributor": { + "McpServerErrorState": { "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "$ref": "#/$defs/ToolCallContributorKind.MCP" + "$ref": "#/$defs/McpServerStatus.Error" }, - "customizationId": { - "type": "string", - "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." } }, "required": [ "kind", - "customizationId" + "error" ] }, - "ToolCallBase": { + "McpServerStoppedState": { "type": "object", - "description": "Metadata common to all tool call states.", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "$ref": "#/$defs/McpServerStatus.Stopped" } }, "required": [ - "toolCallId", - "toolName", - "displayName" + "kind" ] }, - "ToolCallParameterFields": { + "ChatState": { "type": "object", - "description": "Properties available once tool call parameters are fully received.", + "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", "properties": { - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" }, - "toolInput": { + "title": { "type": "string", - "description": "Raw tool input" - } - }, - "required": [ - "invocationMessage" - ] - }, - "ToolCallResult": { - "type": "object", - "description": "Tool execution result details, available after execution completes.", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Chat title" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" }, - "content": { + "activity": { + "type": "string", + "description": "Human-readable description of what the chat is currently doing" + }, + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" + }, + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." + }, + "turns": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/Turn" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "description": "Completed turns" }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "activeTurn": { + "$ref": "#/$defs/ActiveTurn", + "description": "Currently in-progress turn" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "steeringMessage": { + "$ref": "#/$defs/PendingMessage", + "description": "Message to inject into the current turn at a convenient point" + }, + "queuedMessages": { + "type": "array", + "items": { + "$ref": "#/$defs/PendingMessage" }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "description": "Messages to send automatically as new turns after the current turn finishes" + }, + "inputRequests": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputRequest" + }, + "description": "Requests for user input that are currently blocking or informing chat progress" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this chat." } }, "required": [ - "success", - "pastTenseMessage" + "resource", + "title", + "status", + "modifiedAt", + "turns" ] }, - "ToolCallStreamingState": { + "ChatSummary": { "type": "object", - "description": "LM is streaming the tool call parameters.", + "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", "properties": { - "toolCallId": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" + }, + "title": { "type": "string", - "description": "Unique tool call identifier" + "description": "Chat title" }, - "toolName": { + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" + }, + "activity": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Human-readable description of what the chat is currently doing" }, - "displayName": { + "modifiedAt": { "type": "string", - "description": "Human-readable tool name" + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional per-chat model override (defaults to the session's model)" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional per-chat agent override (defaults to the session's agent)" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Streaming" + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" }, - "partialInput": { + "workingDirectory": { + "$ref": "#/$defs/URI", + "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." + } + }, + "required": [ + "resource", + "title", + "status", + "modifiedAt" + ] + }, + "PendingMessage": { + "type": "object", + "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", + "properties": { + "id": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Unique identifier for this pending message" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Progress message shown while parameters are streaming" + "message": { + "$ref": "#/$defs/Message", + "description": "The message that will start the next turn" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "status" + "id", + "message" ] }, - "ToolCallPendingConfirmationState": { + "ChatInputOption": { "type": "object", - "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "description": "A choice in a select-style question.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable option identifier; for MCP enum values this is the enum string" }, - "toolName": { + "label": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Display label" }, - "displayName": { + "description": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Optional secondary text" }, - "toolInput": { + "recommended": { + "type": "boolean", + "description": "Whether this option is the recommended/default choice" + } + }, + "required": [ + "id", + "label" + ] + }, + "ChatInputQuestionBase": { + "type": "object", + "properties": { + "id": { "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + "description": "Stable question identifier used as the key in `answers`" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "title": { + "type": "string", + "description": "Short display title" }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "string" - } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "editable": { + "required": { "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ConfirmationOption" - }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "description": "Whether the user must answer this question to accept the request" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status" + "id", + "message" ] }, - "ToolCallRunningState": { + "ChatInputTextQuestion": { "type": "object", - "description": "Tool is actively executing.", + "description": "Text question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "description": "Prompt shown to the user" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Text" }, - "toolInput": { + "format": { "type": "string", - "description": "Raw tool input" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Running" + "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "min": { + "type": "number", + "description": "Minimum string length" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "max": { + "type": "number", + "description": "Maximum string length" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + "defaultValue": { + "type": "string", + "description": "Default text" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "confirmed" + "id", + "message", + "kind" ] }, - "ToolCallPendingResultConfirmationState": { + "ChatInputNumberQuestion": { "type": "object", - "description": "Tool finished executing, waiting for client to approve the result.", + "description": "Numeric question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" + "description": "Prompt shown to the user" }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "kind": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputQuestionKind.Number" + }, + { + "$ref": "#/$defs/ChatInputQuestionKind.Integer" + } + ] }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "min": { + "type": "number", + "description": "Minimum value" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "max": { + "type": "number", + "description": "Maximum value" }, - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "defaultValue": { + "type": "number", + "description": "Default numeric value" + } + }, + "required": [ + "id", + "message", + "kind" + ] + }, + "ChatInputBooleanQuestion": { + "type": "object", + "description": "Boolean question within a chat input request.", + "properties": { + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "title": { + "type": "string", + "description": "Short display title" }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.Boolean" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "defaultValue": { + "type": "boolean", + "description": "Default boolean value" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id", + "message", + "kind" ] }, - "ToolCallCompletedState": { + "ChatInputSingleSelectQuestion": { "type": "object", - "description": "Tool completed successfully or with an error.", + "description": "Single-select question within a chat input request.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { + "id": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Stable question identifier used as the key in `answers`" }, - "displayName": { + "title": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Short display title" }, - "toolInput": { + "message": { "type": "string", - "description": "Raw tool input" + "description": "Prompt shown to the user" }, - "success": { + "required": { "type": "boolean", - "description": "Whether the tool succeeded" + "description": "Whether the user must answer this question to accept the request" }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.SingleSelect" }, - "content": { + "options": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChatInputOption" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" - }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Completed" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "description": "Options the user may select from" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text instead of selecting an option" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "status", - "confirmed" + "id", + "message", + "kind", + "options" ] }, - "ToolCallCancelledState": { + "ChatInputMultiSelectQuestion": { "type": "object", - "description": "Tool call was cancelled before execution.", + "description": "Multi-select question within a chat input request.", "properties": { - "toolCallId": { + "id": { "type": "string", - "description": "Unique tool call identifier" + "description": "Stable question identifier used as the key in `answers`" }, - "toolName": { + "title": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Short display title" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable tool name" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Prompt shown to the user" }, - "toolInput": { - "type": "string", - "description": "Raw tool input" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "status": { - "$ref": "#/$defs/ToolCallStatus.Cancelled" + "kind": { + "$ref": "#/$defs/ChatInputQuestionKind.MultiSelect" }, - "reason": { - "$ref": "#/$defs/ToolCallCancellationReason", - "description": "Why the tool was cancelled" + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputOption" + }, + "description": "Options the user may select from" }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional message explaining the cancellation" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text in addition to selecting options" }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" + "min": { + "type": "number", + "description": "Minimum selected item count" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "max": { + "type": "number", + "description": "Maximum selected item count" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "reason" + "id", + "message", + "kind", + "options" ] }, - "ToolDefinition": { + "ChatInputRequest": { "type": "object", - "description": "Describes a tool available in a session, provided by either the server or the active client.", + "description": "A live request for user input.\n\nThe server creates or replaces requests with `chat/inputRequested`.\nClients sync drafts with `chat/inputAnswerChanged` and complete requests\nwith `chat/inputCompleted`.", "properties": { - "name": { + "id": { "type": "string", - "description": "Unique tool identifier" + "description": "Stable request identifier" }, - "title": { + "message": { "type": "string", - "description": "Human-readable display name" + "description": "Display message for the request as a whole" }, - "description": { - "type": "string", - "description": "Description of what the tool does" + "url": { + "$ref": "#/$defs/URI", + "description": "URL the user should review or open, for URL-style elicitations" }, - "inputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } + "questions": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputQuestion" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + "description": "Ordered questions to ask the user" }, - "outputSchema": { + "answers": { "type": "object", - "properties": { - "type": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "required": { - "type": "string" - } + "additionalProperties": { + "$ref": "#/$defs/ChatInputAnswer" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." - }, - "annotations": { - "$ref": "#/$defs/ToolAnnotations", - "description": "Behavioral hints about the tool. All properties are advisory." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." + "description": "Current draft or submitted answers, keyed by question ID" } }, "required": [ - "name" + "id" ] }, - "ToolAnnotations": { + "ChatInputTextAnswerValue": { "type": "object", - "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", + "description": "Value captured for one answer.", "properties": { - "title": { - "type": "string", - "description": "Alternate human-readable title" - }, - "readOnlyHint": { - "type": "boolean", - "description": "Tool does not modify its environment (default: false)" - }, - "destructiveHint": { - "type": "boolean", - "description": "Tool may perform destructive updates (default: true)" - }, - "idempotentHint": { - "type": "boolean", - "description": "Repeated calls with the same arguments have no additional effect (default: false)" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Text" }, - "openWorldHint": { - "type": "boolean", - "description": "Tool may interact with external entities (default: true)" + "value": { + "type": "string" } - } + }, + "required": [ + "kind", + "value" + ] }, - "ToolResultTextContent": { + "ChatInputNumberAnswerValue": { "type": "object", - "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Text" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Number" }, - "text": { - "type": "string", - "description": "The text content" + "value": { + "type": "number" } }, "required": [ - "type", - "text" + "kind", + "value" ] }, - "ToolResultEmbeddedResourceContent": { + "ChatInputBooleanAnswerValue": { "type": "object", - "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" - }, - "data": { - "type": "string", - "description": "Base64-encoded data" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Boolean" }, - "contentType": { - "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "value": { + "type": "boolean" } }, "required": [ - "type", - "data", - "contentType" + "kind", + "value" ] }, - "ToolResultResourceContent": { + "ChatInputSelectedAnswerValue": { "type": "object", - "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.Selected" }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "value": { + "type": "string" }, - "type": { - "$ref": "#/$defs/ToolResultContentType.Resource" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered instead of selecting an option" } }, "required": [ - "uri", - "type" + "kind", + "value" ] }, - "ToolResultFileEditContent": { + "ChatInputSelectedManyAnswerValue": { "type": "object", - "description": "Describes a file modification performed by a tool.", "properties": { - "before": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." + "kind": { + "$ref": "#/$defs/ChatInputAnswerValueKind.SelectedMany" }, - "after": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "content": { - "type": "string" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." + "value": { + "type": "array", + "items": { + "type": "string" + } }, - "diff": { - "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } + "freeformValues": { + "type": "array", + "items": { + "type": "string" }, - "description": "Optional diff display metadata" - }, - "type": { - "$ref": "#/$defs/ToolResultContentType.FileEdit" + "description": "Free-form text entered in addition to selected options" } }, "required": [ - "type" + "kind", + "value" ] }, - "ToolResultTerminalContent": { + "ChatInputAnswered": { "type": "object", - "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Terminal" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "state": { + "oneOf": [ + { + "$ref": "#/$defs/ChatInputAnswerState.Draft" + }, + { + "$ref": "#/$defs/ChatInputAnswerState.Submitted" + } + ], + "description": "Answer state" }, - "title": { - "type": "string", - "description": "Display title for the terminal content" + "value": { + "$ref": "#/$defs/ChatInputAnswerValue", + "description": "Answer value" } }, "required": [ - "type", - "resource", - "title" + "state", + "value" ] }, - "ToolResultSubagentContent": { + "ChatInputSkipped": { "type": "object", - "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "type": { - "$ref": "#/$defs/ToolResultContentType.Subagent" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Subagent session URI (subscribable for full session state)" - }, - "title": { - "type": "string", - "description": "Display title for the subagent" - }, - "agentName": { - "type": "string", - "description": "Internal agent name" + "state": { + "$ref": "#/$defs/ChatInputAnswerState.Skipped", + "description": "Answer state" }, - "description": { - "type": "string", - "description": "Human-readable description of the subagent's task" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form reason or value captured while skipping, if any" } }, "required": [ - "type", - "resource", - "title" + "state" ] }, - "CustomizationBase": { + "Turn": { "type": "object", - "description": "Fields shared by every customization variant.", + "description": "A completed request/response cycle.", "properties": { "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Turn identifier" }, - "name": { - "type": "string", - "description": "Human-readable name." + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "icons": { + "responseParts": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ResponsePart" }, - "description": "Icons for UI display." + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - } + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" + }, + "state": { + "$ref": "#/$defs/TurnState", + "description": "How the turn ended" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details if state is `'error'`" + } }, "required": [ "id", - "uri", - "name" + "message", + "responseParts", + "usage", + "state" ] }, - "CustomizationLoadingState": { + "ActiveTurn": { "type": "object", - "description": "Container is being loaded by the host.", + "description": "An in-progress turn — the assistant is actively streaming.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loading" + "id": { + "type": "string", + "description": "Turn identifier" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" + }, + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" } }, "required": [ - "kind" + "id", + "message", + "responseParts", + "usage" ] }, - "CustomizationLoadedState": { + "Message": { "type": "object", - "description": "Container loaded successfully.", + "description": "A message that initiates or steers a turn. Messages can originate from the\nuser or be system-generated (see {@link MessageKind}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Loaded" + "text": { + "type": "string", + "description": "Message text" + }, + "origin": { + "type": "object", + "properties": { + "kind": { + "type": "string" + } + }, + "required": [ + "kind" + ], + "description": "The origin of the message" + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/$defs/MessageAttachment" + }, + "description": "File/selection attachments" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." } }, "required": [ - "kind" + "text", + "origin" ] }, - "CustomizationDegradedState": { + "MessageAttachmentBase": { "type": "object", - "description": "Container partially loaded but has warnings.", + "description": "Common fields shared by all {@link MessageAttachment} variants.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Degraded" + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "message": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { "type": "string", - "description": "Human-readable description of the warning." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." } }, "required": [ - "kind", - "message" + "label" ] }, - "CustomizationErrorState": { + "SimpleMessageAttachment": { "type": "object", - "description": "Container failed to load.", + "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", "properties": { - "kind": { - "$ref": "#/$defs/CustomizationLoadStatus.Error" + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "message": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { "type": "string", - "description": "Human-readable error message." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Simple", + "description": "Discriminant" + }, + "modelRepresentation": { + "type": "string", + "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." } }, "required": [ - "kind", - "message" + "label", + "type" ] }, - "ContainerCustomizationBase": { + "MessageEmbeddedResourceAttachment": { "type": "object", - "description": "Fields shared by container customizations.", + "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", "properties": { - "id": { + "label": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "name": { + "displayKind": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "type": { + "$ref": "#/$defs/MessageAttachmentKind.EmbeddedResource", + "description": "Discriminant" }, - "clientId": { + "data": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Base64-encoded binary data" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "contentType": { + "type": "string", + "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", - "uri", - "name", - "enabled" + "label", + "type", + "data", + "contentType" ] }, - "PluginCustomization": { + "MessageResourceAttachment": { "type": "object", - "description": "An [Open Plugins](https://open-plugins.com/) plugin.", + "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", "properties": { - "id": { + "label": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "name": { + "displayKind": { "type": "string", - "description": "Human-readable name." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "clientId": { + "contentType": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "description": "Content MIME type" }, "type": { - "$ref": "#/$defs/CustomizationType.Plugin" + "$ref": "#/$defs/MessageAttachmentKind.Resource", + "description": "Discriminant" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", + "label", "uri", - "name", - "enabled", "type" ] }, - "ClientPluginCustomization": { + "MessageAnnotationsAttachment": { "type": "object", - "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", + "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { + "label": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, "range": { "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "clientId": { + "displayKind": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "children": { + "type": { + "$ref": "#/$defs/MessageAttachmentKind.Annotations", + "description": "Discriminant" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + }, + "annotationIds": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "type": "string" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Plugin" - }, - "nonce": { - "type": "string", - "description": "Opaque version token used by the host to detect changes." + "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "label", + "type", + "resource" ] }, - "DirectoryCustomization": { + "MarkdownResponsePart": { "type": "object", - "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "kind": { + "$ref": "#/$defs/ResponsePartKind.Markdown", + "description": "Discriminant" }, - "name": { + "id": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "Part identifier, used by `chat/delta` to target this part for content appends" }, - "clientId": { + "content": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Directory" - }, - "contents": { - "$ref": "#/$defs/ChildCustomizationType", - "description": "Which child customization type this directory holds." - }, - "writable": { - "type": "boolean", - "description": "Whether clients may write into this directory." + "description": "Markdown content" } }, "required": [ + "kind", "id", - "uri", - "name", - "enabled", - "type", - "contents", - "writable" + "content" ] }, - "AgentCustomization": { + "ResourceReponsePart": { "type": "object", - "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", + "description": "A content part that's a reference to large content stored outside the state tree.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, "uri": { "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "Content URI" }, - "type": { - "$ref": "#/$defs/CustomizationType.Agent" + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "description": { + "contentType": { "type": "string", - "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + "description": "Content MIME type" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this custom agent.\n\nMirrors the MCP `_meta` convention." + "kind": { + "$ref": "#/$defs/ResponsePartKind.ContentRef", + "description": "Discriminant" } }, "required": [ - "id", "uri", - "name", - "type" + "kind" ] }, - "SkillCustomization": { + "ToolCallResponsePart": { "type": "object", - "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", + "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Skill" - }, - "description": { - "type": "string", - "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + "kind": { + "$ref": "#/$defs/ResponsePartKind.ToolCall", + "description": "Discriminant" }, - "disableModelInvocation": { - "type": "boolean", - "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "Full tool call lifecycle state" } }, "required": [ - "id", - "uri", - "name", - "type" + "kind", + "toolCall" ] }, - "PromptCustomization": { + "ReasoningResponsePart": { "type": "object", - "description": "A prompt contributed by a plugin or directory.", + "description": "Reasoning/thinking content from the model.", "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.Reasoning", + "description": "Discriminant" + }, "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "content": { + "type": "string", + "description": "Accumulated reasoning text" + } + }, + "required": [ + "kind", + "id", + "content" + ] + }, + "SystemNotificationResponsePart": { + "type": "object", + "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", + "properties": { + "kind": { + "$ref": "#/$defs/ResponsePartKind.SystemNotification", + "description": "Discriminant" + }, + "content": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "The text of the system notification" + } + }, + "required": [ + "kind", + "content" + ] + }, + "ConfirmationOption": { + "type": "object", + "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the option, returned in the confirmed action" + }, + "label": { + "type": "string", + "description": "Human-readable label displayed to the user" + }, + "kind": { + "$ref": "#/$defs/ConfirmationOptionKind", + "description": "Whether this option represents an approval or denial" + }, + "group": { + "type": "number", + "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + } + }, + "required": [ + "id", + "label", + "kind" + ] + }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.Client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallContributorKind.MCP" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, + "ToolCallBase": { + "type": "object", + "description": "Metadata common to all tool call states.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName" + ] + }, + "ToolCallParameterFields": { + "type": "object", + "description": "Properties available once tool call parameters are fully received.", + "properties": { + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + } + }, + "required": [ + "invocationMessage" + ] + }, + "ToolCallResult": { + "type": "object", + "description": "Tool execution result details, available after execution completes.", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + } + }, + "required": [ + "success", + "pastTenseMessage" + ] + }, + "ToolCallStreamingState": { + "type": "object", + "description": "LM is streaming the tool call parameters.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Streaming" + }, + "partialInput": { + "type": "string", + "description": "Partial parameters accumulated so far" + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Progress message shown while parameters are streaming" + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "status" + ] + }, + "ToolCallPendingConfirmationState": { + "type": "object", + "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingConfirmation" + }, + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "edits": { + "type": "object", + "properties": { + "items": { + "type": "string" + } + }, + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" + }, + "editable": { + "type": "boolean", + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfirmationOption" + }, + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status" + ] + }, + "ToolCallRunningState": { + "type": "object", + "description": "Tool is actively executing.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Running" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" }, - "name": { + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "confirmed" + ] + }, + "ToolCallPendingResultConfirmationState": { + "type": "object", + "description": "Tool finished executing, waiting for client to approve the result.", + "properties": { + "toolCallId": { "type": "string", - "description": "Human-readable name." + "description": "Unique tool call identifier" }, - "icons": { + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Icons for UI display." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "type": { - "$ref": "#/$defs/CustomizationType.Prompt" + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "description": { - "type": "string", - "description": "Short description of what the prompt does." + "status": { + "$ref": "#/$defs/ToolCallStatus.PendingResultConfirmation" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" ] }, - "RuleCustomization": { + "ToolCallCompletedState": { "type": "object", - "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", + "description": "Tool completed successfully or with an error.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "Unique tool call identifier" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." + "description": "Human-readable tool name" }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "$ref": "#/$defs/CustomizationType.Rule" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "description": { + "toolInput": { "type": "string", - "description": "Description of what the rule enforces." + "description": "Raw tool input" }, - "alwaysApply": { + "success": { "type": "boolean", - "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." + "description": "Whether the tool succeeded" }, - "globs": { + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Completed" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "status", + "confirmed" + ] + }, + "ToolCallCancelledState": { + "type": "object", + "description": "Tool call was cancelled before execution.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "type": "string", + "description": "Raw tool input" + }, + "status": { + "$ref": "#/$defs/ToolCallStatus.Cancelled" + }, + "reason": { + "$ref": "#/$defs/ToolCallCancellationReason", + "description": "Why the tool was cancelled" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional message explaining the cancellation" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "id", - "uri", - "name", - "type" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "reason" ] }, - "HookCustomization": { + "ToolResultTextContent": { "type": "object", - "description": "A hook manifest contributed by a plugin or directory.", + "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "type": { + "$ref": "#/$defs/ToolResultContentType.Text" }, - "name": { + "text": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "type": { - "$ref": "#/$defs/CustomizationType.Hook" + "description": "The text content" } }, "required": [ - "id", - "uri", - "name", - "type" + "type", + "text" ] }, - "McpServerCustomization": { + "ToolResultEmbeddedResourceContent": { "type": "object", - "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", + "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, "type": { - "$ref": "#/$defs/CustomizationType.McpServer" - }, - "enabled": { - "type": "boolean", - "description": "Whether this MCP server is currently enabled." - }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "Current lifecycle state of the MCP server." + "$ref": "#/$defs/ToolResultContentType.EmbeddedResource" }, - "channel": { - "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "data": { + "type": "string", + "description": "Base64-encoded data" }, - "mcpApp": { - "$ref": "#/$defs/McpServerCustomizationApps", - "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" } }, "required": [ - "id", - "uri", - "name", "type", - "enabled", - "state" + "data", + "contentType" ] }, - "McpServerCustomizationApps": { + "ToolResultResourceContent": { "type": "object", - "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "capabilities": { - "$ref": "#/$defs/AhpMcpUiHostCapabilities", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.Resource" } }, "required": [ - "capabilities" + "uri", + "type" ] }, - "AhpMcpUiHostCapabilities": { + "ToolResultFileEditContent": { "type": "object", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "description": "Describes a file modification performed by a tool.", "properties": { - "serverTools": { + "before": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." }, - "serverResources": { + "after": { "type": "object", "properties": { - "listChanged": { - "type": "boolean" + "uri": { + "type": "string" + }, + "content": { + "type": "string" } }, - "description": "Producer proxies the MCP `resources/*` methods to the upstream server." - }, - "logging": { - "type": "object", - "additionalProperties": {}, - "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." }, - "sampling": { + "diff": { "type": "object", "properties": { - "tools": { - "type": "string" + "added": { + "type": "number" + }, + "removed": { + "type": "number" } }, - "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." - } - } - }, - "McpServerStartingState": { - "type": "object", - "description": "Server is registered with the host but has not yet started.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Starting" - } - }, - "required": [ - "kind" - ] - }, - "McpServerReadyState": { - "type": "object", - "description": "Server is running and serving requests.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Ready" + "description": "Optional diff display metadata" + }, + "type": { + "$ref": "#/$defs/ToolResultContentType.FileEdit" } }, "required": [ - "kind" + "type" ] }, - "McpServerAuthRequiredState": { + "ToolResultTerminalContent": { "type": "object", - "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.AuthRequired" - }, - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "type": { + "$ref": "#/$defs/ToolResultContentType.Terminal" }, "resource": { - "$ref": "#/$defs/ProtectedResourceMetadata", - "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." - }, - "requiredScopes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." - }, - "description": { - "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." - } - }, - "required": [ - "kind", - "reason", - "resource" - ] - }, - "McpServerErrorState": { - "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", - "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Error" + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details." + "title": { + "type": "string", + "description": "Display title for the terminal content" } }, "required": [ - "kind", - "error" + "type", + "resource", + "title" ] }, - "McpServerStoppedState": { + "ToolResultSubagentContent": { "type": "object", - "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", + "description": "A reference to a subagent session spawned by a tool.\n\nClients can subscribe to the subagent's session URI to stream its\nprogress in real time, including inner tool calls and responses.", "properties": { - "kind": { - "$ref": "#/$defs/McpServerStatus.Stopped" + "type": { + "$ref": "#/$defs/ToolResultContentType.Subagent" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "Subagent session URI (subscribable for full session state)" + }, + "title": { + "type": "string", + "description": "Display title for the subagent" + }, + "agentName": { + "type": "string", + "description": "Internal agent name" + }, + "description": { + "type": "string", + "description": "Human-readable description of the subagent's task" } }, "required": [ - "kind" + "type", + "resource", + "title" ] }, "TerminalInfo": { @@ -3941,253 +4055,307 @@ ], "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." }, - "SessionInputQuestion": { + "ChildCustomizationType": { "oneOf": [ + {}, + { + "$ref": "#/$defs/CustomizationType.Agent" + }, { - "$ref": "#/$defs/SessionInputTextQuestion" + "$ref": "#/$defs/CustomizationType.Skill" }, { - "$ref": "#/$defs/SessionInputNumberQuestion" + "$ref": "#/$defs/CustomizationType.Prompt" }, { - "$ref": "#/$defs/SessionInputBooleanQuestion" + "$ref": "#/$defs/CustomizationType.Rule" }, { - "$ref": "#/$defs/SessionInputSingleSelectQuestion" + "$ref": "#/$defs/CustomizationType.Hook" }, { - "$ref": "#/$defs/SessionInputMultiSelectQuestion" + "$ref": "#/$defs/CustomizationType.McpServer" } ], - "description": "One question within a session input request." + "description": "Customization types that appear as children of a\n{@link PluginCustomization} or {@link DirectoryCustomization}." }, - "SessionInputAnswerValue": { + "CustomizationLoadState": { "oneOf": [ + {}, { - "$ref": "#/$defs/SessionInputTextAnswerValue" - }, - { - "$ref": "#/$defs/SessionInputNumberAnswerValue" + "$ref": "#/$defs/CustomizationLoadingState" }, { - "$ref": "#/$defs/SessionInputBooleanAnswerValue" + "$ref": "#/$defs/CustomizationLoadedState" }, { - "$ref": "#/$defs/SessionInputSelectedAnswerValue" + "$ref": "#/$defs/CustomizationDegradedState" }, { - "$ref": "#/$defs/SessionInputSelectedManyAnswerValue" + "$ref": "#/$defs/CustomizationErrorState" } - ] + ], + "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." }, - "SessionInputAnswer": { + "ChildCustomization": { "oneOf": [ + {}, + { + "$ref": "#/$defs/AgentCustomization" + }, + { + "$ref": "#/$defs/SkillCustomization" + }, + { + "$ref": "#/$defs/PromptCustomization" + }, { - "$ref": "#/$defs/SessionInputAnswered" + "$ref": "#/$defs/RuleCustomization" + }, + { + "$ref": "#/$defs/HookCustomization" }, { - "$ref": "#/$defs/SessionInputSkipped" + "$ref": "#/$defs/McpServerCustomization" } ], - "description": "Draft, submitted, or skipped answer for one question." + "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." }, - "MessageAttachment": { + "Customization": { "oneOf": [ {}, { - "$ref": "#/$defs/SimpleMessageAttachment" - }, - { - "$ref": "#/$defs/MessageEmbeddedResourceAttachment" + "$ref": "#/$defs/PluginCustomization" }, { - "$ref": "#/$defs/MessageResourceAttachment" + "$ref": "#/$defs/DirectoryCustomization" }, { - "$ref": "#/$defs/MessageAnnotationsAttachment" + "$ref": "#/$defs/McpServerCustomization" } ], - "description": "An attachment associated with a {@link Message}." + "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." }, - "ResponsePart": { + "McpServerState": { "oneOf": [ {}, { - "$ref": "#/$defs/MarkdownResponsePart" + "$ref": "#/$defs/McpServerStartingState" }, { - "$ref": "#/$defs/ResourceReponsePart" + "$ref": "#/$defs/McpServerReadyState" }, { - "$ref": "#/$defs/ToolCallResponsePart" + "$ref": "#/$defs/McpServerAuthRequiredState" }, { - "$ref": "#/$defs/ReasoningResponsePart" + "$ref": "#/$defs/McpServerErrorState" }, { - "$ref": "#/$defs/SystemNotificationResponsePart" + "$ref": "#/$defs/McpServerStoppedState" } - ] + ], + "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, - "ToolCallContributor": { + "ChatOrigin": { "oneOf": [ + {}, { - "$ref": "#/$defs/ToolCallClientContributor" + "type": "object", + "properties": { + "kind": { + "type": "string" + } + }, + "required": [ + "kind" + ] }, { - "$ref": "#/$defs/ToolCallMcpContributor" + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "chat": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "kind", + "chat", + "turnId" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "chat": { + "type": "string" + }, + "toolCallId": { + "type": "string" + } + }, + "required": [ + "kind", + "chat", + "toolCallId" + ] } ] }, - "ToolCallState": { + "ChatInputQuestion": { "oneOf": [ - {}, { - "$ref": "#/$defs/ToolCallStreamingState" - }, - { - "$ref": "#/$defs/ToolCallPendingConfirmationState" + "$ref": "#/$defs/ChatInputTextQuestion" }, { - "$ref": "#/$defs/ToolCallRunningState" + "$ref": "#/$defs/ChatInputNumberQuestion" }, { - "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + "$ref": "#/$defs/ChatInputBooleanQuestion" }, { - "$ref": "#/$defs/ToolCallCompletedState" + "$ref": "#/$defs/ChatInputSingleSelectQuestion" }, { - "$ref": "#/$defs/ToolCallCancelledState" + "$ref": "#/$defs/ChatInputMultiSelectQuestion" } ], - "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." + "description": "One question within a chat input request." }, - "ToolResultContent": { + "ChatInputAnswerValue": { "oneOf": [ - {}, - { - "$ref": "#/$defs/ToolResultTextContent" - }, { - "$ref": "#/$defs/ToolResultEmbeddedResourceContent" + "$ref": "#/$defs/ChatInputTextAnswerValue" }, { - "$ref": "#/$defs/ToolResultResourceContent" + "$ref": "#/$defs/ChatInputNumberAnswerValue" }, { - "$ref": "#/$defs/ToolResultFileEditContent" + "$ref": "#/$defs/ChatInputBooleanAnswerValue" }, { - "$ref": "#/$defs/ToolResultTerminalContent" + "$ref": "#/$defs/ChatInputSelectedAnswerValue" }, { - "$ref": "#/$defs/ToolResultSubagentContent" + "$ref": "#/$defs/ChatInputSelectedManyAnswerValue" } - ], - "description": "Content block in a tool result.\n\nMirrors the content blocks in MCP `CallToolResult.content`, plus\n`ToolResultResourceContent` for lazy-loading large results,\n`ToolResultFileEditContent` for file edit diffs,\n`ToolResultTerminalContent` for live terminal output, and\n`ToolResultSubagentContent` for subagent sessions (AHP extensions)." + ] }, - "ChildCustomizationType": { + "ChatInputAnswer": { "oneOf": [ - {}, - { - "$ref": "#/$defs/CustomizationType.Agent" - }, - { - "$ref": "#/$defs/CustomizationType.Skill" - }, - { - "$ref": "#/$defs/CustomizationType.Prompt" - }, - { - "$ref": "#/$defs/CustomizationType.Rule" - }, { - "$ref": "#/$defs/CustomizationType.Hook" + "$ref": "#/$defs/ChatInputAnswered" }, { - "$ref": "#/$defs/CustomizationType.McpServer" + "$ref": "#/$defs/ChatInputSkipped" } ], - "description": "Customization types that appear as children of a\n{@link PluginCustomization} or {@link DirectoryCustomization}." + "description": "Draft, submitted, or skipped answer for one question." }, - "CustomizationLoadState": { + "MessageAttachment": { "oneOf": [ {}, { - "$ref": "#/$defs/CustomizationLoadingState" + "$ref": "#/$defs/SimpleMessageAttachment" }, { - "$ref": "#/$defs/CustomizationLoadedState" + "$ref": "#/$defs/MessageEmbeddedResourceAttachment" }, { - "$ref": "#/$defs/CustomizationDegradedState" + "$ref": "#/$defs/MessageResourceAttachment" }, { - "$ref": "#/$defs/CustomizationErrorState" + "$ref": "#/$defs/MessageAnnotationsAttachment" } ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + "description": "An attachment associated with a {@link Message}." }, - "ChildCustomization": { + "ResponsePart": { "oneOf": [ {}, { - "$ref": "#/$defs/AgentCustomization" + "$ref": "#/$defs/MarkdownResponsePart" }, { - "$ref": "#/$defs/SkillCustomization" + "$ref": "#/$defs/ResourceReponsePart" }, { - "$ref": "#/$defs/PromptCustomization" + "$ref": "#/$defs/ToolCallResponsePart" }, { - "$ref": "#/$defs/RuleCustomization" + "$ref": "#/$defs/ReasoningResponsePart" }, { - "$ref": "#/$defs/HookCustomization" + "$ref": "#/$defs/SystemNotificationResponsePart" + } + ] + }, + "ToolCallContributor": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallClientContributor" }, { - "$ref": "#/$defs/McpServerCustomization" + "$ref": "#/$defs/ToolCallMcpContributor" } - ], - "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." + ] }, - "Customization": { + "ToolCallState": { "oneOf": [ {}, { - "$ref": "#/$defs/PluginCustomization" + "$ref": "#/$defs/ToolCallStreamingState" }, { - "$ref": "#/$defs/DirectoryCustomization" + "$ref": "#/$defs/ToolCallPendingConfirmationState" }, { - "$ref": "#/$defs/McpServerCustomization" + "$ref": "#/$defs/ToolCallRunningState" + }, + { + "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + }, + { + "$ref": "#/$defs/ToolCallCompletedState" + }, + { + "$ref": "#/$defs/ToolCallCancelledState" } ], - "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." + "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." }, - "McpServerState": { + "ToolResultContent": { "oneOf": [ {}, { - "$ref": "#/$defs/McpServerStartingState" + "$ref": "#/$defs/ToolResultTextContent" }, { - "$ref": "#/$defs/McpServerReadyState" + "$ref": "#/$defs/ToolResultEmbeddedResourceContent" }, { - "$ref": "#/$defs/McpServerAuthRequiredState" + "$ref": "#/$defs/ToolResultResourceContent" }, { - "$ref": "#/$defs/McpServerErrorState" + "$ref": "#/$defs/ToolResultFileEditContent" }, { - "$ref": "#/$defs/McpServerStoppedState" + "$ref": "#/$defs/ToolResultTerminalContent" + }, + { + "$ref": "#/$defs/ToolResultSubagentContent" } ], - "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." + "description": "Content block in a tool result.\n\nMirrors the content blocks in MCP `CallToolResult.content`, plus\n`ToolResultResourceContent` for lazy-loading large results,\n`ToolResultFileEditContent` for file edit diffs,\n`ToolResultTerminalContent` for live terminal output, and\n`ToolResultSubagentContent` for subagent sessions (AHP extensions)." }, "TerminalClaim": { "oneOf": [ diff --git a/scripts/find-protocol-sources.ts b/scripts/find-protocol-sources.ts index 40777394..862088bd 100644 --- a/scripts/find-protocol-sources.ts +++ b/scripts/find-protocol-sources.ts @@ -18,6 +18,7 @@ export const PROTOCOL_SOURCE_DIRS: readonly string[] = [ 'common', 'channels-root', 'channels-session', + 'channels-chat', 'channels-terminal', 'channels-changeset', 'channels-annotations', diff --git a/scripts/generate-action-origin.ts b/scripts/generate-action-origin.ts index b641f496..0b66aa73 100644 --- a/scripts/generate-action-origin.ts +++ b/scripts/generate-action-origin.ts @@ -17,7 +17,7 @@ const GENERATED_HEADER = `// Generated from types/actions.ts — do not edit // Run \`npm run generate\` to regenerate. `; -type ActionScope = 'root' | 'session' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch'; +type ActionScope = 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch'; interface ActionInfo { /** The interface name (e.g. 'RootAgentsChangedAction') */ @@ -150,6 +150,7 @@ export function generateActionOrigin(project: Project, outDir: string): void { const category = getJsDocTag(node as any, 'category') || ''; const scope: ActionScope = category === 'Root Actions' ? 'root' + : category === 'Chat Actions' ? 'chat' : category === 'Terminal Actions' ? 'terminal' : category === 'Changeset Actions' ? 'changeset' : category === 'Annotations Actions' ? 'annotations' @@ -201,6 +202,7 @@ export function generateActionOrigin(project: Project, outDir: string): void { // Generate output const rootActions = actions.filter(a => a.scope === 'root'); const sessionActions = actions.filter(a => a.scope === 'session'); + const chatActions = actions.filter(a => a.scope === 'chat'); const terminalActions = actions.filter(a => a.scope === 'terminal'); const changesetActions = actions.filter(a => a.scope === 'changeset'); const annotationsActions = actions.filter(a => a.scope === 'annotations'); @@ -209,6 +211,8 @@ export function generateActionOrigin(project: Project, outDir: string): void { const serverRootActions = rootActions.filter(a => !a.isClientDispatchable); const clientSessionActions = sessionActions.filter(a => a.isClientDispatchable); const serverSessionActions = sessionActions.filter(a => !a.isClientDispatchable); + const clientChatActions = chatActions.filter(a => a.isClientDispatchable); + const serverChatActions = chatActions.filter(a => !a.isClientDispatchable); const clientTerminalActions = terminalActions.filter(a => a.isClientDispatchable); const serverTerminalActions = terminalActions.filter(a => !a.isClientDispatchable); const clientChangesetActions = changesetActions.filter(a => a.isClientDispatchable); @@ -232,7 +236,7 @@ export function generateActionOrigin(project: Project, outDir: string): void { lines.push(``); // RootAction - lines.push(`// ─── Root vs Session vs Terminal vs Changeset Action Unions ─────────────────`); + lines.push(`// ─── Root vs Session vs Chat vs Terminal vs Changeset Action Unions ─────────────────`); lines.push(``); lines.push(`/** Union of all root-scoped actions. */`); lines.push(`export type RootAction =`); @@ -287,6 +291,33 @@ export function generateActionOrigin(project: Project, outDir: string): void { lines.push(`;`); lines.push(``); + // ChatAction + lines.push(`/** Union of all chat-scoped actions. */`); + lines.push(`export type ChatAction =`); + for (let i = 0; i < chatActions.length; i++) { + lines.push(` | ${chatActions[i].name}`); + } + lines.push(`;`); + lines.push(``); + + // ClientChatAction + lines.push(`/** Union of chat actions that clients may dispatch. */`); + lines.push(`export type ClientChatAction =`); + for (let i = 0; i < clientChatActions.length; i++) { + lines.push(` | ${clientChatActions[i].name}`); + } + lines.push(`;`); + lines.push(``); + + // ServerChatAction + lines.push(`/** Union of chat actions that only the server may produce. */`); + lines.push(`export type ServerChatAction =`); + for (let i = 0; i < serverChatActions.length; i++) { + lines.push(` | ${serverChatActions[i].name}`); + } + lines.push(`;`); + lines.push(``); + // TerminalAction lines.push(`/** Union of all terminal-scoped actions. */`); lines.push(`export type TerminalAction =`); diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index b76d6182..676030e2 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -162,9 +162,11 @@ function mapType(tsType: string): string { tsType === 'IRootState | ISessionState | ITerminalState' || tsType === 'RootState | SessionState' || tsType === 'RootState | SessionState | TerminalState' || - tsType === 'RootState | SessionState | TerminalState | ChangesetState' - || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' - || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' + tsType === 'RootState | SessionState | TerminalState | ChangesetState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || + tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' || + tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState' ) { return 'SnapshotState'; } @@ -632,9 +634,9 @@ function generateDiscriminatedUnion(cfg: UnionConfig): string { // ─── State File Generator ──────────────────────────────────────────────────── const STATE_ENUMS = [ - 'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus', - 'SessionInputAnswerState', 'SessionInputAnswerValueKind', 'SessionInputQuestionKind', - 'SessionInputResponseKind', + 'PolicyState', 'SessionLifecycle', 'SessionStatus', + 'ChatOriginKind', 'PendingMessageKind', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', + 'ChatInputResponseKind', 'TurnState', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -654,11 +656,13 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'AgentSelection' }, { name: 'ConfigPropertySchema' }, { name: 'ConfigSchema' }, - { name: 'PendingMessage' }, { name: 'SessionState' }, { name: 'SessionActiveClient' }, { name: 'SessionSummary' }, { name: 'ChangesSummary' }, + { name: 'ChatState' }, + { name: 'ChatSummary' }, + { name: 'PendingMessage' }, { name: 'ProjectInfo' }, { name: 'SessionConfigPropertySchema' }, { name: 'SessionConfigSchema' }, @@ -666,20 +670,20 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'Turn' }, { name: 'ActiveTurn' }, { name: 'Message' }, - { name: 'SessionInputOption' }, - { name: 'SessionInputTextAnswerValue' }, - { name: 'SessionInputNumberAnswerValue' }, - { name: 'SessionInputBooleanAnswerValue' }, - { name: 'SessionInputSelectedAnswerValue' }, - { name: 'SessionInputSelectedManyAnswerValue' }, - { name: 'SessionInputAnswered' }, - { name: 'SessionInputSkipped' }, - { name: 'SessionInputTextQuestion' }, - { name: 'SessionInputNumberQuestion' }, - { name: 'SessionInputBooleanQuestion' }, - { name: 'SessionInputSingleSelectQuestion' }, - { name: 'SessionInputMultiSelectQuestion' }, - { name: 'SessionInputRequest' }, + { name: 'ChatInputOption' }, + { name: 'ChatInputTextAnswerValue' }, + { name: 'ChatInputNumberAnswerValue' }, + { name: 'ChatInputBooleanAnswerValue' }, + { name: 'ChatInputSelectedAnswerValue' }, + { name: 'ChatInputSelectedManyAnswerValue' }, + { name: 'ChatInputAnswered' }, + { name: 'ChatInputSkipped' }, + { name: 'ChatInputTextQuestion' }, + { name: 'ChatInputNumberQuestion' }, + { name: 'ChatInputBooleanQuestion' }, + { name: 'ChatInputSingleSelectQuestion' }, + { name: 'ChatInputMultiSelectQuestion' }, + { name: 'ChatInputRequest' }, { name: 'TextPosition' }, { name: 'TextRange' }, { name: 'TextSelection' }, @@ -805,43 +809,43 @@ const TERMINAL_CONTENT_PART_UNION: UnionConfig = { unknown: true, }; -const SESSION_INPUT_QUESTION_UNION: UnionConfig = { - name: 'SessionInputQuestion', +const CHAT_INPUT_QUESTION_UNION: UnionConfig = { + name: 'ChatInputQuestion', discriminantField: 'kind', - doc: 'SessionInputQuestion is one question within a session input request.', + doc: 'ChatInputQuestion is one question within a chat input request.', variants: [ - { variantName: 'Text', innerType: 'SessionInputTextQuestion', wireValue: 'text' }, - { variantName: 'Number', innerType: 'SessionInputNumberQuestion', wireValue: 'number' }, - { variantName: 'Integer', innerType: 'SessionInputNumberQuestion', wireValue: 'integer' }, - { variantName: 'Boolean', innerType: 'SessionInputBooleanQuestion', wireValue: 'boolean' }, - { variantName: 'SingleSelect', innerType: 'SessionInputSingleSelectQuestion', wireValue: 'single-select' }, - { variantName: 'MultiSelect', innerType: 'SessionInputMultiSelectQuestion', wireValue: 'multi-select' }, + { variantName: 'Text', innerType: 'ChatInputTextQuestion', wireValue: 'text' }, + { variantName: 'Number', innerType: 'ChatInputNumberQuestion', wireValue: 'number' }, + { variantName: 'Integer', innerType: 'ChatInputNumberQuestion', wireValue: 'integer' }, + { variantName: 'Boolean', innerType: 'ChatInputBooleanQuestion', wireValue: 'boolean' }, + { variantName: 'SingleSelect', innerType: 'ChatInputSingleSelectQuestion', wireValue: 'single-select' }, + { variantName: 'MultiSelect', innerType: 'ChatInputMultiSelectQuestion', wireValue: 'multi-select' }, ], unknown: true, }; -const SESSION_INPUT_ANSWER_VALUE_UNION: UnionConfig = { - name: 'SessionInputAnswerValue', +const CHAT_INPUT_ANSWER_VALUE_UNION: UnionConfig = { + name: 'ChatInputAnswerValue', discriminantField: 'kind', - doc: 'SessionInputAnswerValue is the value captured for one answer.', + doc: 'ChatInputAnswerValue is the value captured for one answer.', variants: [ - { variantName: 'Text', innerType: 'SessionInputTextAnswerValue', wireValue: 'text' }, - { variantName: 'Number', innerType: 'SessionInputNumberAnswerValue', wireValue: 'number' }, - { variantName: 'Boolean', innerType: 'SessionInputBooleanAnswerValue', wireValue: 'boolean' }, - { variantName: 'Selected', innerType: 'SessionInputSelectedAnswerValue', wireValue: 'selected' }, - { variantName: 'SelectedMany', innerType: 'SessionInputSelectedManyAnswerValue', wireValue: 'selected-many' }, + { variantName: 'Text', innerType: 'ChatInputTextAnswerValue', wireValue: 'text' }, + { variantName: 'Number', innerType: 'ChatInputNumberAnswerValue', wireValue: 'number' }, + { variantName: 'Boolean', innerType: 'ChatInputBooleanAnswerValue', wireValue: 'boolean' }, + { variantName: 'Selected', innerType: 'ChatInputSelectedAnswerValue', wireValue: 'selected' }, + { variantName: 'SelectedMany', innerType: 'ChatInputSelectedManyAnswerValue', wireValue: 'selected-many' }, ], unknown: true, }; -const SESSION_INPUT_ANSWER_UNION: UnionConfig = { - name: 'SessionInputAnswer', +const CHAT_INPUT_ANSWER_UNION: UnionConfig = { + name: 'ChatInputAnswer', discriminantField: 'state', - doc: 'SessionInputAnswer is a draft, submitted, or skipped answer for one question.', + doc: 'ChatInputAnswer is a draft, submitted, or skipped answer for one question.', variants: [ - { variantName: 'Draft', innerType: 'SessionInputAnswered', wireValue: 'draft' }, - { variantName: 'Submitted', innerType: 'SessionInputAnswered', wireValue: 'submitted' }, - { variantName: 'Skipped', innerType: 'SessionInputSkipped', wireValue: 'skipped' }, + { variantName: 'Draft', innerType: 'ChatInputAnswered', wireValue: 'draft' }, + { variantName: 'Submitted', innerType: 'ChatInputAnswered', wireValue: 'submitted' }, + { variantName: 'Skipped', innerType: 'ChatInputSkipped', wireValue: 'skipped' }, ], unknown: true, }; @@ -939,15 +943,99 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +function generateChatOriginGo(): string { + return `// ChatOrigin describes how a chat came into existence. +type ChatOrigin struct { +\tValue isChatOrigin +} + +// isChatOrigin is the marker interface for chat origin variants. +type isChatOrigin interface{ isChatOrigin() } + +type ChatUserOrigin struct { +\tKind ChatOriginKind \`json:"kind"\` +} + +func (*ChatUserOrigin) isChatOrigin() {} + +type ChatForkOrigin struct { +\tKind ChatOriginKind \`json:"kind"\` +\tChat URI \`json:"chat"\` +\tTurnId string \`json:"turnId"\` +} + +func (*ChatForkOrigin) isChatOrigin() {} + +type ChatToolOrigin struct { +\tKind ChatOriginKind \`json:"kind"\` +\tChat URI \`json:"chat"\` +\tToolCallId string \`json:"toolCallId"\` +} + +func (*ChatToolOrigin) isChatOrigin() {} + +type ChatOriginUnknown struct { +\tRaw json.RawMessage +} + +func (*ChatOriginUnknown) isChatOrigin() {} + +func (o *ChatOrigin) UnmarshalJSON(data []byte) error { +\tdisc, _, err := readDiscriminator(data, "kind") +\tif err != nil { +\t\treturn err +\t} +\tswitch disc { +\tcase "user": +\t\tvar v ChatUserOrigin +\t\tif err := json.Unmarshal(data, &v); err != nil { +\t\t\treturn err +\t\t} +\t\to.Value = &v +\tcase "fork": +\t\tvar v ChatForkOrigin +\t\tif err := json.Unmarshal(data, &v); err != nil { +\t\t\treturn err +\t\t} +\t\to.Value = &v +\tcase "tool": +\t\tvar v ChatToolOrigin +\t\tif err := json.Unmarshal(data, &v); err != nil { +\t\t\treturn err +\t\t} +\t\to.Value = &v +\tdefault: +\t\traw := make(json.RawMessage, len(data)) +\t\tcopy(raw, data) +\t\to.Value = &ChatOriginUnknown{Raw: raw} +\t} +\treturn nil +} + +func (o ChatOrigin) MarshalJSON() ([]byte, error) { +\tif unk, ok := o.Value.(*ChatOriginUnknown); ok { +\t\tif len(unk.Raw) == 0 { +\t\t\treturn []byte("null"), nil +\t\t} +\t\treturn unk.Raw, nil +\t} +\tif o.Value == nil { +\t\treturn []byte("null"), nil +\t} +\treturn json.Marshal(o.Value) +}`; +} + function generateSnapshotState(): string { return `// SnapshotState is the state payload of a snapshot — root, session, -// terminal, changeset, resource-watch, or annotations state. The active +// chat, terminal, changeset, resource-watch, or annotations state. The active // variant is chosen by which pointer field is non-nil; UnmarshalJSON probes // for required fields in the canonical order -// (session → terminal → changeset → resourceWatch → annotations → root). +// (session → chat → terminal → changeset → resourceWatch → annotations → root). type SnapshotState struct { \tRoot *RootState \`json:"-"\` \tSession *SessionState \`json:"-"\` +\tChat *ChatState \`json:"-"\` \tTerminal *TerminalState \`json:"-"\` \tChangeset *ChangesetState \`json:"-"\` \tResourceWatch *ResourceWatchState \`json:"-"\` @@ -959,6 +1047,8 @@ func (s SnapshotState) MarshalJSON() ([]byte, error) { \tswitch { \tcase s.Session != nil: \t\treturn json.Marshal(s.Session) +\tcase s.Chat != nil: +\t\treturn json.Marshal(s.Chat) \tcase s.Terminal != nil: \t\treturn json.Marshal(s.Terminal) \tcase s.Changeset != nil: @@ -989,6 +1079,12 @@ func (s *SnapshotState) UnmarshalJSON(data []byte) error { \t\t\treturn err \t\t} \t\ts.Session = &v +\tcase containsAll(probe, "summary", "turns"): +\t\tvar v ChatState +\t\tif err := json.Unmarshal(data, &v); err != nil { +\t\t\treturn err +\t\t} +\t\ts.Chat = &v \tcase containsAll(probe, "content"): \t\tvar v TerminalState \t\tif err := json.Unmarshal(data, &v); err != nil { @@ -1069,11 +1165,11 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CONTENT_PART_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(SESSION_INPUT_QUESTION_UNION)); + lines.push(generateDiscriminatedUnion(CHAT_INPUT_QUESTION_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(SESSION_INPUT_ANSWER_VALUE_UNION)); + lines.push(generateDiscriminatedUnion(CHAT_INPUT_ANSWER_VALUE_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(SESSION_INPUT_ANSWER_UNION)); + lines.push(generateDiscriminatedUnion(CHAT_INPUT_ANSWER_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_RESULT_CONTENT_UNION)); lines.push(''); @@ -1089,6 +1185,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateChatOriginGo()); + lines.push(''); lines.push(generateSnapshotState()); lines.push(''); @@ -1107,21 +1205,33 @@ const ACTION_VARIANTS: { { type: 'root/configChanged', variantName: 'RootConfigChanged', tsInterface: 'RootConfigChangedAction' }, { type: 'session/ready', variantName: 'SessionReady', tsInterface: 'SessionReadyAction' }, { type: 'session/creationFailed', variantName: 'SessionCreationFailed', tsInterface: 'SessionCreationFailedAction' }, - { type: 'session/turnStarted', variantName: 'SessionTurnStarted', tsInterface: 'SessionTurnStartedAction' }, - { type: 'session/delta', variantName: 'SessionDelta', tsInterface: 'SessionDeltaAction' }, - { type: 'session/responsePart', variantName: 'SessionResponsePart', tsInterface: 'SessionResponsePartAction' }, - { type: 'session/toolCallStart', variantName: 'SessionToolCallStart', tsInterface: 'SessionToolCallStartAction' }, - { type: 'session/toolCallDelta', variantName: 'SessionToolCallDelta', tsInterface: 'SessionToolCallDeltaAction' }, - { type: 'session/toolCallReady', variantName: 'SessionToolCallReady', tsInterface: 'SessionToolCallReadyAction' }, - { type: 'session/toolCallConfirmed', variantName: 'SessionToolCallConfirmed', tsInterface: '_merged_' }, - { type: 'session/toolCallComplete', variantName: 'SessionToolCallComplete', tsInterface: 'SessionToolCallCompleteAction' }, - { type: 'session/toolCallResultConfirmed', variantName: 'SessionToolCallResultConfirmed', tsInterface: 'SessionToolCallResultConfirmedAction' }, - { type: 'session/turnComplete', variantName: 'SessionTurnComplete', tsInterface: 'SessionTurnCompleteAction' }, - { type: 'session/turnCancelled', variantName: 'SessionTurnCancelled', tsInterface: 'SessionTurnCancelledAction' }, - { type: 'session/error', variantName: 'SessionError', tsInterface: 'SessionErrorAction' }, + { type: 'session/chatAdded', variantName: 'SessionChatAdded', tsInterface: 'SessionChatAddedAction' }, + { type: 'session/chatRemoved', variantName: 'SessionChatRemoved', tsInterface: 'SessionChatRemovedAction' }, + { type: 'session/chatUpdated', variantName: 'SessionChatUpdated', tsInterface: 'SessionChatUpdatedAction' }, + { type: 'session/defaultChatChanged', variantName: 'SessionDefaultChatChanged', tsInterface: 'SessionDefaultChatChangedAction' }, + { type: 'chat/turnStarted', variantName: 'ChatTurnStarted', tsInterface: 'ChatTurnStartedAction' }, + { type: 'chat/delta', variantName: 'ChatDelta', tsInterface: 'ChatDeltaAction' }, + { type: 'chat/responsePart', variantName: 'ChatResponsePart', tsInterface: 'ChatResponsePartAction' }, + { type: 'chat/toolCallStart', variantName: 'ChatToolCallStart', tsInterface: 'ChatToolCallStartAction' }, + { type: 'chat/toolCallDelta', variantName: 'ChatToolCallDelta', tsInterface: 'ChatToolCallDeltaAction' }, + { type: 'chat/toolCallReady', variantName: 'ChatToolCallReady', tsInterface: 'ChatToolCallReadyAction' }, + { type: 'chat/toolCallConfirmed', variantName: 'ChatToolCallConfirmed', tsInterface: '_chat_tool_call_confirmed_' }, + { type: 'chat/toolCallComplete', variantName: 'ChatToolCallComplete', tsInterface: 'ChatToolCallCompleteAction' }, + { type: 'chat/toolCallResultConfirmed', variantName: 'ChatToolCallResultConfirmed', tsInterface: 'ChatToolCallResultConfirmedAction' }, + { type: 'chat/toolCallContentChanged', variantName: 'ChatToolCallContentChanged', tsInterface: 'ChatToolCallContentChangedAction' }, + { type: 'chat/turnComplete', variantName: 'ChatTurnComplete', tsInterface: 'ChatTurnCompleteAction' }, + { type: 'chat/turnCancelled', variantName: 'ChatTurnCancelled', tsInterface: 'ChatTurnCancelledAction' }, + { type: 'chat/error', variantName: 'ChatError', tsInterface: 'ChatErrorAction' }, { type: 'session/titleChanged', variantName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, - { type: 'session/usage', variantName: 'SessionUsage', tsInterface: 'SessionUsageAction' }, - { type: 'session/reasoning', variantName: 'SessionReasoning', tsInterface: 'SessionReasoningAction' }, + { type: 'chat/usage', variantName: 'ChatUsage', tsInterface: 'ChatUsageAction' }, + { type: 'chat/reasoning', variantName: 'ChatReasoning', tsInterface: 'ChatReasoningAction' }, + { type: 'chat/pendingMessageSet', variantName: 'ChatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, + { type: 'chat/pendingMessageRemoved', variantName: 'ChatPendingMessageRemoved', tsInterface: 'ChatPendingMessageRemovedAction' }, + { type: 'chat/queuedMessagesReordered', variantName: 'ChatQueuedMessagesReordered', tsInterface: 'ChatQueuedMessagesReorderedAction' }, + { type: 'chat/inputRequested', variantName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, + { type: 'chat/inputAnswerChanged', variantName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, + { type: 'chat/inputCompleted', variantName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, + { type: 'chat/truncated', variantName: 'ChatTruncated', tsInterface: 'ChatTruncatedAction' }, { type: 'session/modelChanged', variantName: 'SessionModelChanged', tsInterface: 'SessionModelChangedAction' }, { type: 'session/agentChanged', variantName: 'SessionAgentChanged', tsInterface: 'SessionAgentChangedAction' }, { type: 'session/isReadChanged', variantName: 'SessionIsReadChanged', tsInterface: 'SessionIsReadChangedAction' }, @@ -1131,21 +1241,13 @@ const ACTION_VARIANTS: { { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientChanged', variantName: 'SessionActiveClientChanged', tsInterface: 'SessionActiveClientChangedAction' }, { type: 'session/activeClientToolsChanged', variantName: 'SessionActiveClientToolsChanged', tsInterface: 'SessionActiveClientToolsChangedAction' }, - { type: 'session/pendingMessageSet', variantName: 'SessionPendingMessageSet', tsInterface: 'SessionPendingMessageSetAction' }, - { type: 'session/pendingMessageRemoved', variantName: 'SessionPendingMessageRemoved', tsInterface: 'SessionPendingMessageRemovedAction' }, - { type: 'session/queuedMessagesReordered', variantName: 'SessionQueuedMessagesReordered', tsInterface: 'SessionQueuedMessagesReorderedAction' }, - { type: 'session/inputRequested', variantName: 'SessionInputRequested', tsInterface: 'SessionInputRequestedAction' }, - { type: 'session/inputAnswerChanged', variantName: 'SessionInputAnswerChanged', tsInterface: 'SessionInputAnswerChangedAction' }, - { type: 'session/inputCompleted', variantName: 'SessionInputCompleted', tsInterface: 'SessionInputCompletedAction' }, { type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, { type: 'session/customizationRemoved', variantName: 'SessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, { type: 'session/mcpServerStateChanged', variantName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, - { type: 'session/truncated', variantName: 'SessionTruncated', tsInterface: 'SessionTruncatedAction' }, { type: 'session/configChanged', variantName: 'SessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', variantName: 'SessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, - { type: 'session/toolCallContentChanged', variantName: 'SessionToolCallContentChanged', tsInterface: 'SessionToolCallContentChangedAction' }, { type: 'changeset/statusChanged', variantName: 'ChangesetStatusChanged', tsInterface: 'ChangesetStatusChangedAction' }, { type: 'changeset/fileSet', variantName: 'ChangesetFileSet', tsInterface: 'ChangesetFileSetAction' }, { type: 'changeset/fileRemoved', variantName: 'ChangesetFileRemoved', tsInterface: 'ChangesetFileRemovedAction' }, @@ -1172,10 +1274,10 @@ const ACTION_VARIANTS: { { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, ]; -function generateMergedToolCallConfirmedStruct(): string { - return `// SessionToolCallConfirmedAction is the client approves or denies a +function generateMergedChatToolCallConfirmedStruct(): string { + return `// ChatToolCallConfirmedAction is the client approves or denies a // pending tool call (merged approved + denied variants on the wire). -type SessionToolCallConfirmedAction struct { +type ChatToolCallConfirmedAction struct { \tType ActionType \`json:"type"\` \tTurnId string \`json:"turnId"\` \tToolCallId string \`json:"toolCallId"\` @@ -1217,8 +1319,8 @@ function generateActionsUnion(): string { variants: ACTION_VARIANTS.map((v) => ({ variantName: v.variantName, innerType: - v.tsInterface === '_merged_' - ? 'SessionToolCallConfirmedAction' + v.tsInterface === '_chat_tool_call_confirmed_' + ? 'ChatToolCallConfirmedAction' : stripIPrefix(v.tsInterface), wireValue: v.type, })), @@ -1242,8 +1344,8 @@ function generateActionsFile(project: Project): string { lines.push('// ─── Action Payloads ─────────────────────────────────────────────────\n'); for (const v of ACTION_VARIANTS) { - if (v.tsInterface === '_merged_') { - lines.push(generateMergedToolCallConfirmedStruct()); + if (v.tsInterface === '_chat_tool_call_confirmed_') { + lines.push(generateMergedChatToolCallConfirmedStruct()); lines.push(''); continue; } @@ -1280,6 +1382,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'SubscribeParams' }, { name: 'SubscribeResult' }, { name: 'SessionForkSource' }, { name: 'CreateSessionParams' }, { name: 'DisposeSessionParams' }, + { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, { name: 'DisposeChatParams' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, { name: 'ResourceReadParams' }, { name: 'ResourceReadResult' }, { name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' }, @@ -1742,7 +1845,7 @@ function checkExhaustiveness(project: Project): void { ...COMMAND_ENUMS, ...NOTIFICATION_STRUCTS, ...NOTIFICATION_ENUMS, - ...ACTION_VARIANTS.filter((v) => v.tsInterface !== '_merged_').map((v) => v.tsInterface), + ...ACTION_VARIANTS.filter((v) => !v.tsInterface.startsWith('_')).map((v) => v.tsInterface), ]); const knownSpecial = new Set([ @@ -1758,12 +1861,17 @@ function checkExhaustiveness(project: Project): void { 'SessionToolCallApprovedAction', 'SessionToolCallDeniedAction', 'SessionToolCallConfirmedAction', + 'ChatToolCallApprovedAction', + 'ChatToolCallDeniedAction', + 'ChatToolCallConfirmedAction', + 'ChatAction', 'PingParams', 'TerminalClaim', 'TerminalContentPart', - 'SessionInputQuestion', - 'SessionInputAnswerValue', - 'SessionInputAnswer', + 'ChatOrigin', + 'ChatInputQuestion', + 'ChatInputAnswerValue', + 'ChatInputAnswer', 'MessageAttachment', 'MessageAttachmentBase', 'Customization', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 86e29d7a..c786e3bb 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -140,9 +140,13 @@ function mapType(tsType: string): string { if ( tsType === 'RootState | SessionState' || tsType === 'RootState | SessionState | TerminalState' || - tsType === 'RootState | SessionState | TerminalState | ChangesetState' - || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' - || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' + tsType === 'RootState | SessionState | TerminalState | ChangesetState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || + tsType === 'RootState | SessionState | ChatState' || + tsType === 'RootState | SessionState | ChatState | TerminalState' || + tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' || + tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState' ) { return 'SnapshotState'; } @@ -469,8 +473,8 @@ interface UnionConfig { * and is responsible for its own discriminator field on the wire. * * Multiple discriminant wire values may map to the same `structName` (e.g. - * `SessionInputQuestion` accepts both "number" and "integer" → the same - * `SessionInputNumberQuestion` data class). We deduplicate variants by + * `ChatInputQuestion` accepts both "number" and "integer" → the same + * `ChatInputNumberQuestion` data class). We deduplicate variants by * `structName` for the sealed-interface declaration but preserve every entry * in the deserializer switch. */ @@ -638,13 +642,14 @@ internal object StringOrMarkdownSerializer : KSerializer { function generateSnapshotState(): string { return `/** - * The state payload of a snapshot — root, session, terminal, changeset, + * The state payload of a snapshot — root, session, chat, terminal, changeset, * resource-watch, or annotations state. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { @JvmInline value class Root(val value: RootState) : SnapshotState @JvmInline value class Session(val value: SessionState) : SnapshotState + @JvmInline value class Chat(val value: ChatState) : SnapshotState @JvmInline value class Terminal(val value: TerminalState) : SnapshotState @JvmInline value class Changeset(val value: ChangesetState) : SnapshotState @JvmInline value class ResourceWatch(val value: ResourceWatchState) : SnapshotState @@ -686,6 +691,7 @@ internal object SnapshotStateSerializer : KSerializer { val element: JsonElement = when (value) { is SnapshotState.Root -> output.json.encodeToJsonElement(RootState.serializer(), value.value) is SnapshotState.Session -> output.json.encodeToJsonElement(SessionState.serializer(), value.value) + is SnapshotState.Chat -> output.json.encodeToJsonElement(ChatState.serializer(), value.value) is SnapshotState.Terminal -> output.json.encodeToJsonElement(TerminalState.serializer(), value.value) is SnapshotState.Changeset -> output.json.encodeToJsonElement(ChangesetState.serializer(), value.value) is SnapshotState.ResourceWatch -> output.json.encodeToJsonElement(ResourceWatchState.serializer(), value.value) @@ -759,8 +765,8 @@ internal object ToolResultContentSerializer : KSerializer { const STATE_ENUMS = [ 'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus', - 'SessionInputAnswerState', 'SessionInputAnswerValueKind', 'SessionInputQuestionKind', - 'SessionInputResponseKind', + 'ChatOriginKind', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', + 'ChatInputResponseKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -772,17 +778,17 @@ const STATE_ENUMS = [ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', - 'PendingMessage', 'SessionState', 'SessionActiveClient', + 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', 'SessionSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', - 'SessionInputOption', - 'SessionInputTextAnswerValue', 'SessionInputNumberAnswerValue', - 'SessionInputBooleanAnswerValue', 'SessionInputSelectedAnswerValue', - 'SessionInputSelectedManyAnswerValue', 'SessionInputAnswered', - 'SessionInputSkipped', - 'SessionInputTextQuestion', - 'SessionInputNumberQuestion', 'SessionInputBooleanQuestion', - 'SessionInputSingleSelectQuestion', 'SessionInputMultiSelectQuestion', - 'SessionInputRequest', + 'ChatInputOption', + 'ChatInputTextAnswerValue', 'ChatInputNumberAnswerValue', + 'ChatInputBooleanAnswerValue', 'ChatInputSelectedAnswerValue', + 'ChatInputSelectedManyAnswerValue', 'ChatInputAnswered', + 'ChatInputSkipped', + 'ChatInputTextQuestion', + 'ChatInputNumberQuestion', 'ChatInputBooleanQuestion', + 'ChatInputSingleSelectQuestion', 'ChatInputMultiSelectQuestion', + 'ChatInputRequest', 'TextPosition', 'TextRange', 'TextSelection', 'SimpleMessageAttachment', 'MessageEmbeddedResourceAttachment', 'MessageResourceAttachment', 'MessageAnnotationsAttachment', @@ -862,42 +868,98 @@ const TERMINAL_CONTENT_PART_UNION: UnionConfig = { unknown: true, }; +function generateChatOriginKotlin(): string { + return `@Serializable(with = ChatOriginSerializer::class) +sealed interface ChatOrigin { + @JvmInline value class User(val value: ChatOriginUser) : ChatOrigin + @JvmInline value class Fork(val value: ChatOriginFork) : ChatOrigin + @JvmInline value class Tool(val value: ChatOriginTool) : ChatOrigin + @JvmInline value class Unknown(val raw: JsonObject) : ChatOrigin +} + +@Serializable +data class ChatOriginUser( + val kind: ChatOriginKind = ChatOriginKind.USER, +) + +@Serializable +data class ChatOriginFork( + val kind: ChatOriginKind = ChatOriginKind.FORK, + val chat: String, + val turnId: String, +) + +@Serializable +data class ChatOriginTool( + val kind: ChatOriginKind = ChatOriginKind.TOOL, + val chat: String, + val toolCallId: String, +) + +internal object ChatOriginSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ChatOrigin") + + override fun deserialize(decoder: Decoder): ChatOrigin { + val input = decoder as? JsonDecoder ?: error("ChatOrigin can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject ?: error("Expected JsonObject for ChatOrigin") + return when ((obj["kind"] as? JsonPrimitive)?.contentOrNull) { + "user" -> ChatOrigin.User(input.json.decodeFromJsonElement(ChatOriginUser.serializer(), element)) + "fork" -> ChatOrigin.Fork(input.json.decodeFromJsonElement(ChatOriginFork.serializer(), element)) + "tool" -> ChatOrigin.Tool(input.json.decodeFromJsonElement(ChatOriginTool.serializer(), element)) + else -> ChatOrigin.Unknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: ChatOrigin) { + val output = encoder as? JsonEncoder ?: error("ChatOrigin can only be serialized to JSON") + val element: JsonElement = when (value) { + is ChatOrigin.User -> output.json.encodeToJsonElement(ChatOriginUser.serializer(), value.value) + is ChatOrigin.Fork -> output.json.encodeToJsonElement(ChatOriginFork.serializer(), value.value) + is ChatOrigin.Tool -> output.json.encodeToJsonElement(ChatOriginTool.serializer(), value.value) + is ChatOrigin.Unknown -> value.raw + } + output.encodeJsonElement(element) + } +}`; +} + const SESSION_INPUT_QUESTION_UNION: UnionConfig = { - name: 'SessionInputQuestion', + name: 'ChatInputQuestion', discriminantField: 'kind', variants: [ - { caseName: 'Text', structName: 'SessionInputTextQuestion', discriminantValue: 'text' }, + { caseName: 'Text', structName: 'ChatInputTextQuestion', discriminantValue: 'text' }, // Both "number" and "integer" wire values map to the same data class. // Generator deduplicates the sealed-interface variant by struct name. - { caseName: 'Number', structName: 'SessionInputNumberQuestion', discriminantValue: 'number' }, - { caseName: 'Number', structName: 'SessionInputNumberQuestion', discriminantValue: 'integer' }, - { caseName: 'Boolean', structName: 'SessionInputBooleanQuestion', discriminantValue: 'boolean' }, - { caseName: 'SingleSelect', structName: 'SessionInputSingleSelectQuestion', discriminantValue: 'single-select' }, - { caseName: 'MultiSelect', structName: 'SessionInputMultiSelectQuestion', discriminantValue: 'multi-select' }, + { caseName: 'Number', structName: 'ChatInputNumberQuestion', discriminantValue: 'number' }, + { caseName: 'Number', structName: 'ChatInputNumberQuestion', discriminantValue: 'integer' }, + { caseName: 'Boolean', structName: 'ChatInputBooleanQuestion', discriminantValue: 'boolean' }, + { caseName: 'SingleSelect', structName: 'ChatInputSingleSelectQuestion', discriminantValue: 'single-select' }, + { caseName: 'MultiSelect', structName: 'ChatInputMultiSelectQuestion', discriminantValue: 'multi-select' }, ], unknown: true, }; const SESSION_INPUT_ANSWER_VALUE_UNION: UnionConfig = { - name: 'SessionInputAnswerValue', + name: 'ChatInputAnswerValue', discriminantField: 'kind', variants: [ - { caseName: 'Text', structName: 'SessionInputTextAnswerValue', discriminantValue: 'text' }, - { caseName: 'Number', structName: 'SessionInputNumberAnswerValue', discriminantValue: 'number' }, - { caseName: 'Boolean', structName: 'SessionInputBooleanAnswerValue', discriminantValue: 'boolean' }, - { caseName: 'Selected', structName: 'SessionInputSelectedAnswerValue', discriminantValue: 'selected' }, - { caseName: 'SelectedMany', structName: 'SessionInputSelectedManyAnswerValue', discriminantValue: 'selected-many' }, + { caseName: 'Text', structName: 'ChatInputTextAnswerValue', discriminantValue: 'text' }, + { caseName: 'Number', structName: 'ChatInputNumberAnswerValue', discriminantValue: 'number' }, + { caseName: 'Boolean', structName: 'ChatInputBooleanAnswerValue', discriminantValue: 'boolean' }, + { caseName: 'Selected', structName: 'ChatInputSelectedAnswerValue', discriminantValue: 'selected' }, + { caseName: 'SelectedMany', structName: 'ChatInputSelectedManyAnswerValue', discriminantValue: 'selected-many' }, ], unknown: true, }; const SESSION_INPUT_ANSWER_UNION: UnionConfig = { - name: 'SessionInputAnswer', + name: 'ChatInputAnswer', discriminantField: 'state', variants: [ - { caseName: 'Draft', structName: 'SessionInputAnswered', discriminantValue: 'draft' }, - { caseName: 'Submitted', structName: 'SessionInputAnswered', discriminantValue: 'submitted' }, - { caseName: 'Skipped', structName: 'SessionInputSkipped', discriminantValue: 'skipped' }, + { caseName: 'Draft', structName: 'ChatInputAnswered', discriminantValue: 'draft' }, + { caseName: 'Submitted', structName: 'ChatInputAnswered', discriminantValue: 'submitted' }, + { caseName: 'Skipped', structName: 'ChatInputSkipped', discriminantValue: 'skipped' }, ], unknown: true, }; @@ -1011,6 +1073,8 @@ function generateStateFile(project: Project): string { lines.push('// ─── Discriminated Unions ───────────────────────────────────────────────────'); lines.push(''); + lines.push(generateChatOriginKotlin()); + lines.push(''); lines.push(generateDiscriminatedUnion(RESPONSE_PART_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_STATE_UNION)); @@ -1052,21 +1116,26 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'root/activeSessionsChanged', caseName: 'RootActiveSessionsChanged', tsInterface: 'RootActiveSessionsChangedAction' }, { type: 'session/ready', caseName: 'SessionReady', tsInterface: 'SessionReadyAction' }, { type: 'session/creationFailed', caseName: 'SessionCreationFailed', tsInterface: 'SessionCreationFailedAction' }, - { type: 'session/turnStarted', caseName: 'SessionTurnStarted', tsInterface: 'SessionTurnStartedAction' }, - { type: 'session/delta', caseName: 'SessionDelta', tsInterface: 'SessionDeltaAction' }, - { type: 'session/responsePart', caseName: 'SessionResponsePart', tsInterface: 'SessionResponsePartAction' }, - { type: 'session/toolCallStart', caseName: 'SessionToolCallStart', tsInterface: 'SessionToolCallStartAction' }, - { type: 'session/toolCallDelta', caseName: 'SessionToolCallDelta', tsInterface: 'SessionToolCallDeltaAction' }, - { type: 'session/toolCallReady', caseName: 'SessionToolCallReady', tsInterface: 'SessionToolCallReadyAction' }, - { type: 'session/toolCallConfirmed', caseName: 'SessionToolCallConfirmed', tsInterface: '_merged_' }, - { type: 'session/toolCallComplete', caseName: 'SessionToolCallComplete', tsInterface: 'SessionToolCallCompleteAction' }, - { type: 'session/toolCallResultConfirmed', caseName: 'SessionToolCallResultConfirmed', tsInterface: 'SessionToolCallResultConfirmedAction' }, - { type: 'session/turnComplete', caseName: 'SessionTurnComplete', tsInterface: 'SessionTurnCompleteAction' }, - { type: 'session/turnCancelled', caseName: 'SessionTurnCancelled', tsInterface: 'SessionTurnCancelledAction' }, - { type: 'session/error', caseName: 'SessionError', tsInterface: 'SessionErrorAction' }, + { type: 'session/chatAdded', caseName: 'SessionChatAdded', tsInterface: 'SessionChatAddedAction' }, + { type: 'session/chatRemoved', caseName: 'SessionChatRemoved', tsInterface: 'SessionChatRemovedAction' }, + { type: 'session/chatUpdated', caseName: 'SessionChatUpdated', tsInterface: 'SessionChatUpdatedAction' }, + { type: 'session/defaultChatChanged', caseName: 'SessionDefaultChatChanged', tsInterface: 'SessionDefaultChatChangedAction' }, + { type: 'chat/turnStarted', caseName: 'ChatTurnStarted', tsInterface: 'ChatTurnStartedAction' }, + { type: 'chat/delta', caseName: 'ChatDelta', tsInterface: 'ChatDeltaAction' }, + { type: 'chat/responsePart', caseName: 'ChatResponsePart', tsInterface: 'ChatResponsePartAction' }, + { type: 'chat/toolCallStart', caseName: 'ChatToolCallStart', tsInterface: 'ChatToolCallStartAction' }, + { type: 'chat/toolCallDelta', caseName: 'ChatToolCallDelta', tsInterface: 'ChatToolCallDeltaAction' }, + { type: 'chat/toolCallReady', caseName: 'ChatToolCallReady', tsInterface: 'ChatToolCallReadyAction' }, + { type: 'chat/toolCallConfirmed', caseName: 'ChatToolCallConfirmed', tsInterface: '_merged_chat_' }, + { type: 'chat/toolCallComplete', caseName: 'ChatToolCallComplete', tsInterface: 'ChatToolCallCompleteAction' }, + { type: 'chat/toolCallResultConfirmed', caseName: 'ChatToolCallResultConfirmed', tsInterface: 'ChatToolCallResultConfirmedAction' }, + { type: 'chat/toolCallContentChanged', caseName: 'ChatToolCallContentChanged', tsInterface: 'ChatToolCallContentChangedAction' }, + { type: 'chat/turnComplete', caseName: 'ChatTurnComplete', tsInterface: 'ChatTurnCompleteAction' }, + { type: 'chat/turnCancelled', caseName: 'ChatTurnCancelled', tsInterface: 'ChatTurnCancelledAction' }, + { type: 'chat/error', caseName: 'ChatError', tsInterface: 'ChatErrorAction' }, { type: 'session/titleChanged', caseName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, - { type: 'session/usage', caseName: 'SessionUsage', tsInterface: 'SessionUsageAction' }, - { type: 'session/reasoning', caseName: 'SessionReasoning', tsInterface: 'SessionReasoningAction' }, + { type: 'chat/usage', caseName: 'ChatUsage', tsInterface: 'ChatUsageAction' }, + { type: 'chat/reasoning', caseName: 'ChatReasoning', tsInterface: 'ChatReasoningAction' }, { type: 'session/modelChanged', caseName: 'SessionModelChanged', tsInterface: 'SessionModelChangedAction' }, { type: 'session/agentChanged', caseName: 'SessionAgentChanged', tsInterface: 'SessionAgentChangedAction' }, { type: 'session/isReadChanged', caseName: 'SessionIsReadChanged', tsInterface: 'SessionIsReadChangedAction' }, @@ -1076,21 +1145,20 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/serverToolsChanged', caseName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientChanged', caseName: 'SessionActiveClientChanged', tsInterface: 'SessionActiveClientChangedAction' }, { type: 'session/activeClientToolsChanged', caseName: 'SessionActiveClientToolsChanged', tsInterface: 'SessionActiveClientToolsChangedAction' }, - { type: 'session/pendingMessageSet', caseName: 'SessionPendingMessageSet', tsInterface: 'SessionPendingMessageSetAction' }, - { type: 'session/pendingMessageRemoved', caseName: 'SessionPendingMessageRemoved', tsInterface: 'SessionPendingMessageRemovedAction' }, - { type: 'session/queuedMessagesReordered', caseName: 'SessionQueuedMessagesReordered', tsInterface: 'SessionQueuedMessagesReorderedAction' }, - { type: 'session/inputRequested', caseName: 'SessionInputRequested', tsInterface: 'SessionInputRequestedAction' }, - { type: 'session/inputAnswerChanged', caseName: 'SessionInputAnswerChanged', tsInterface: 'SessionInputAnswerChangedAction' }, - { type: 'session/inputCompleted', caseName: 'SessionInputCompleted', tsInterface: 'SessionInputCompletedAction' }, + { type: 'chat/pendingMessageSet', caseName: 'ChatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, + { type: 'chat/pendingMessageRemoved', caseName: 'ChatPendingMessageRemoved', tsInterface: 'ChatPendingMessageRemovedAction' }, + { type: 'chat/queuedMessagesReordered', caseName: 'ChatQueuedMessagesReordered', tsInterface: 'ChatQueuedMessagesReorderedAction' }, + { type: 'chat/inputRequested', caseName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, + { type: 'chat/inputAnswerChanged', caseName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, + { type: 'chat/inputCompleted', caseName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, { type: 'session/customizationsChanged', caseName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', caseName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', caseName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, { type: 'session/customizationRemoved', caseName: 'SessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, { type: 'session/mcpServerStateChanged', caseName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, - { type: 'session/truncated', caseName: 'SessionTruncated', tsInterface: 'SessionTruncatedAction' }, + { type: 'chat/truncated', caseName: 'ChatTruncated', tsInterface: 'ChatTruncatedAction' }, { type: 'session/configChanged', caseName: 'SessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', caseName: 'SessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, - { type: 'session/toolCallContentChanged', caseName: 'SessionToolCallContentChanged', tsInterface: 'SessionToolCallContentChangedAction' }, { type: 'changeset/statusChanged', caseName: 'ChangesetStatusChanged', tsInterface: 'ChangesetStatusChangedAction' }, { type: 'changeset/fileSet', caseName: 'ChangesetFileSet', tsInterface: 'ChangesetFileSetAction' }, { type: 'changeset/fileRemoved', caseName: 'ChangesetFileRemoved', tsInterface: 'ChangesetFileRemovedAction' }, @@ -1119,14 +1187,16 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] ]; /** Merged data class for the approved/denied tool call confirmed action. */ -function generateMergedToolCallConfirmedDataClass(): string { +function generateMergedToolCallConfirmedDataClass(scope: 'Session' | 'Chat' = 'Session'): string { + const className = `${scope}ToolCallConfirmedAction`; + const actionType = scope === 'Chat' ? 'ActionType.CHAT_TOOL_CALL_CONFIRMED' : 'ActionType.SESSION_TOOL_CALL_CONFIRMED'; return `/** * Client approves or denies a pending tool call (merged approved + denied variants). */ @Serializable -data class SessionToolCallConfirmedAction( +data class ${className}( /** Action type discriminant */ - val type: ActionType = ActionType.SESSION_TOOL_CALL_CONFIRMED, + val type: ActionType = ${actionType}, /** Turn identifier */ val turnId: String, /** Tool call identifier */ @@ -1173,9 +1243,10 @@ function generateActionsFile(project: Project): string { // Individual action data classes lines.push('// ─── Action Types ───────────────────────────────────────────────────────────'); lines.push(''); + const priorPartialsAction = new Set(requiredPartialStructs); for (const variant of ACTION_VARIANTS) { - if (variant.tsInterface === '_merged_') { - lines.push(generateMergedToolCallConfirmedDataClass()); + if (variant.tsInterface === '_merged_' || variant.tsInterface === '_merged_chat_') { + lines.push(generateMergedToolCallConfirmedDataClass(variant.tsInterface === '_merged_chat_' ? 'Chat' : 'Session')); lines.push(''); continue; } @@ -1188,6 +1259,24 @@ function generateActionsFile(project: Project): string { } } + // Emit any Partial types referenced only by action payloads (e.g. + // Partial on SessionChatUpdatedAction). Mirrors the + // notification-side emission so action-only partials don't slip through. + const actionNewPartials = [...requiredPartialStructs].filter(n => !priorPartialsAction.has(n)); + if (actionNewPartials.length > 0) { + lines.push('// ─── Partial Summary Types ──────────────────────────────────────────────────'); + lines.push(''); + for (const tsName of actionNewPartials) { + try { + lines.push(generatePartialDataClassFromInterface(project, tsName)); + lines.push(''); + } catch (e) { + lines.push(`// TODO: Could not generate Partial<${tsName}>: ${e}`); + lines.push(''); + } + } + } + // StateAction discriminated union lines.push('// ─── StateAction Union ──────────────────────────────────────────────────────'); lines.push(''); @@ -1204,7 +1293,7 @@ function generateActionsFile(project: Project): string { lines.push('sealed interface StateAction'); lines.push(''); for (const v of ACTION_VARIANTS) { - const dataClass = v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface; + const dataClass = v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface === '_merged_chat_' ? 'ChatToolCallConfirmedAction' : v.tsInterface; lines.push(`@JvmInline value class StateAction${v.caseName}(val value: ${dataClass}) : StateAction`); } lines.push('@JvmInline value class StateActionUnknown(val raw: JsonObject) : StateAction'); @@ -1224,7 +1313,7 @@ function generateActionsFile(project: Project): string { lines.push(' ?: return StateActionUnknown(obj)'); lines.push(' return when (type) {'); for (const v of ACTION_VARIANTS) { - const dataClass = v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface; + const dataClass = v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface === '_merged_chat_' ? 'ChatToolCallConfirmedAction' : v.tsInterface; lines.push(` ${JSON.stringify(v.type)} -> StateAction${v.caseName}(input.json.decodeFromJsonElement(${dataClass}.serializer(), element))`); } lines.push(' else -> StateActionUnknown(obj)'); @@ -1236,7 +1325,7 @@ function generateActionsFile(project: Project): string { lines.push(' ?: error("StateAction can only be serialized to JSON")'); lines.push(' val element: JsonElement = when (value) {'); for (const v of ACTION_VARIANTS) { - const dataClass = v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface; + const dataClass = v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface === '_merged_chat_' ? 'ChatToolCallConfirmedAction' : v.tsInterface; lines.push(` is StateAction${v.caseName} -> output.json.encodeToJsonElement(${dataClass}.serializer(), value.value)`); } lines.push(' is StateActionUnknown -> value.raw'); @@ -1259,6 +1348,7 @@ const COMMAND_STRUCTS = [ 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', + 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', 'ResourceWriteParams', 'ResourceWriteResult', @@ -1754,9 +1844,14 @@ function checkExhaustiveness(project: Project): void { 'SessionToolCallConfirmedAction', // emitted as merged variant 'TerminalClaim', // TERMINAL_CLAIM_UNION discriminated union 'TerminalContentPart', // TERMINAL_CONTENT_PART_UNION discriminated union - 'SessionInputQuestion', // SESSION_INPUT_QUESTION_UNION discriminated union - 'SessionInputAnswerValue', // SESSION_INPUT_ANSWER_VALUE_UNION discriminated union - 'SessionInputAnswer', // SESSION_INPUT_ANSWER_UNION discriminated union + 'ChatInputQuestion', // CHAT_INPUT_QUESTION_UNION discriminated union + 'ChatInputAnswerValue', // CHAT_INPUT_ANSWER_VALUE_UNION discriminated union + 'ChatInputAnswer', // CHAT_INPUT_ANSWER_UNION discriminated union + 'ChatOrigin', // hand-generated union for inline variants + 'ChatToolCallApprovedAction', // merged into ChatToolCallConfirmedAction + 'ChatToolCallDeniedAction', // merged into ChatToolCallConfirmedAction + 'ChatToolCallConfirmedAction', // emitted as merged variant + 'ChatAction', // source-only union covered by StateAction 'MessageAttachment', // MESSAGE_ATTACHMENT_UNION discriminated union 'MessageAttachmentBase', // base interface, flattened into the variant data classes via `extends` 'Customization', // CUSTOMIZATION_UNION discriminated union diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts index 5e83baef..0e08372f 100644 --- a/scripts/generate-markdown.ts +++ b/scripts/generate-markdown.ts @@ -51,6 +51,7 @@ const DIR_TO_PAGE: Record = { 'common': 'common', 'channels-root': 'root', 'channels-session': 'session', + 'channels-chat': 'chat', 'channels-terminal': 'terminal', 'channels-changeset': 'changeset', 'channels-annotations': 'annotations', @@ -946,6 +947,35 @@ function generateSessionChannelPage(project: Project): string { return lines.join('\n'); } +function generateChatChannelPage(project: Project): string { + currentPage = 'chat'; + const stateSf = findChannelSourceFile(project, 'channels-chat', 'state.ts'); + const actionsSf = findChannelSourceFile(project, 'channels-chat', 'actions.ts'); + const commandsSf = findChannelSourceFile(project, 'channels-chat', 'commands.ts'); + + const lines: string[] = [GENERATED_HEADER]; + lines.push('# Chat Channel\n'); + lines.push('Reference for the `ahp-chat:/` channel — per-chat state, the turn lifecycle, tool-call state machine, attachments, pending messages, and input requests. A chat belongs to a session (see [Session Channel](/reference/session)); a session may contain multiple chats. See [Chat Channel specification](/specification/chat-channel) for the wire-level overview.\n'); + lines.push(schemaLink('state.schema.json')); + + if (stateSf) { + lines.push('## State Types\n'); + lines.push(emitStateTypesSection([stateSf])); + } + if (actionsSf) { + lines.push('## Actions\n'); + lines.push('Mutate `ChatState`. Scoped to a chat URI via the enclosing `ActionEnvelope.channel`.\n'); + lines.push(schemaLink('actions.schema.json')); + lines.push(emitActionsSection([actionsSf])); + } + if (commandsSf) { + lines.push('## Commands\n'); + lines.push(schemaLink('commands.schema.json')); + lines.push(emitCommandsSection(project, [commandsSf])); + } + return lines.join('\n'); +} + function generateTerminalChannelPage(project: Project): string { currentPage = 'terminal'; const stateSf = findChannelSourceFile(project, 'channels-terminal', 'state.ts'); @@ -1271,6 +1301,7 @@ export function generateMarkdownDocs(project: Project, outDir: string): void { { filename: 'common.md', generator: generateCommonPage }, { filename: 'root.md', generator: generateRootChannelPage }, { filename: 'session.md', generator: generateSessionChannelPage }, + { filename: 'chat.md', generator: generateChatChannelPage }, { filename: 'terminal.md', generator: generateTerminalChannelPage }, { filename: 'changeset.md', generator: generateChangesetChannelPage }, { filename: 'annotations.md', generator: generateAnnotationsChannelPage }, diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index ff43ec79..eb212f76 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -152,7 +152,11 @@ function mapType(tsType: string, propName?: string, containerName?: string): str || tsType === 'RootState | SessionState' || tsType === 'RootState | SessionState | TerminalState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' - || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState') { + || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' + || tsType === 'RootState | SessionState | ChatState' + || tsType === 'RootState | SessionState | ChatState | TerminalState' + || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' + || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState') { return 'SnapshotState'; } @@ -527,8 +531,8 @@ function generateStructFromInterface( const STATE_ENUMS = [ 'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus', - 'SessionInputAnswerState', 'SessionInputAnswerValueKind', 'SessionInputQuestionKind', - 'SessionInputResponseKind', + 'ChatOriginKind', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', + 'ChatInputResponseKind', 'TurnState', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -554,6 +558,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'ConfigPropertySchema' }, { name: 'ConfigSchema' }, { name: 'PendingMessage' }, + { name: 'ChatState' }, + { name: 'ChatSummary' }, { name: 'SessionState' }, { name: 'SessionActiveClient' }, { name: 'SessionSummary' }, @@ -565,20 +571,20 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'Turn' }, { name: 'ActiveTurn' }, { name: 'Message' }, - { name: 'SessionInputOption' }, - { name: 'SessionInputTextAnswerValue', omitDiscriminants: true }, - { name: 'SessionInputNumberAnswerValue', omitDiscriminants: true }, - { name: 'SessionInputBooleanAnswerValue', omitDiscriminants: true }, - { name: 'SessionInputSelectedAnswerValue', omitDiscriminants: true }, - { name: 'SessionInputSelectedManyAnswerValue', omitDiscriminants: true }, - { name: 'SessionInputAnswered', omitDiscriminants: true }, - { name: 'SessionInputSkipped', omitDiscriminants: true }, - { name: 'SessionInputTextQuestion', omitDiscriminants: true }, - { name: 'SessionInputNumberQuestion', omitDiscriminants: true }, - { name: 'SessionInputBooleanQuestion', omitDiscriminants: true }, - { name: 'SessionInputSingleSelectQuestion', omitDiscriminants: true }, - { name: 'SessionInputMultiSelectQuestion', omitDiscriminants: true }, - { name: 'SessionInputRequest' }, + { name: 'ChatInputOption' }, + { name: 'ChatInputTextAnswerValue', omitDiscriminants: true }, + { name: 'ChatInputNumberAnswerValue', omitDiscriminants: true }, + { name: 'ChatInputBooleanAnswerValue', omitDiscriminants: true }, + { name: 'ChatInputSelectedAnswerValue', omitDiscriminants: true }, + { name: 'ChatInputSelectedManyAnswerValue', omitDiscriminants: true }, + { name: 'ChatInputAnswered', omitDiscriminants: true }, + { name: 'ChatInputSkipped', omitDiscriminants: true }, + { name: 'ChatInputTextQuestion', omitDiscriminants: true }, + { name: 'ChatInputNumberQuestion', omitDiscriminants: true }, + { name: 'ChatInputBooleanQuestion', omitDiscriminants: true }, + { name: 'ChatInputSingleSelectQuestion', omitDiscriminants: true }, + { name: 'ChatInputMultiSelectQuestion', omitDiscriminants: true }, + { name: 'ChatInputRequest' }, { name: 'TextPosition' }, { name: 'TextRange' }, { name: 'TextSelection' }, @@ -704,43 +710,43 @@ const TERMINAL_CONTENT_PART_UNION: UnionConfig = { unknown: true, }; -const SESSION_INPUT_QUESTION_UNION: UnionConfig = { - name: 'SessionInputQuestion', +const CHAT_INPUT_QUESTION_UNION: UnionConfig = { + name: 'ChatInputQuestion', discriminantField: 'kind', - doc: 'One question within a session input request.', + doc: 'One question within a chat input request.', variants: [ - { variantName: 'Text', innerType: 'SessionInputTextQuestion', wireValue: 'text' }, - { variantName: 'Number', innerType: 'SessionInputNumberQuestion', wireValue: 'number' }, - { variantName: 'Integer', innerType: 'SessionInputNumberQuestion', wireValue: 'integer' }, - { variantName: 'Boolean', innerType: 'SessionInputBooleanQuestion', wireValue: 'boolean' }, - { variantName: 'SingleSelect', innerType: 'SessionInputSingleSelectQuestion', wireValue: 'single-select' }, - { variantName: 'MultiSelect', innerType: 'SessionInputMultiSelectQuestion', wireValue: 'multi-select' }, + { variantName: 'Text', innerType: 'ChatInputTextQuestion', wireValue: 'text' }, + { variantName: 'Number', innerType: 'ChatInputNumberQuestion', wireValue: 'number' }, + { variantName: 'Integer', innerType: 'ChatInputNumberQuestion', wireValue: 'integer' }, + { variantName: 'Boolean', innerType: 'ChatInputBooleanQuestion', wireValue: 'boolean' }, + { variantName: 'SingleSelect', innerType: 'ChatInputSingleSelectQuestion', wireValue: 'single-select' }, + { variantName: 'MultiSelect', innerType: 'ChatInputMultiSelectQuestion', wireValue: 'multi-select' }, ], unknown: true, }; -const SESSION_INPUT_ANSWER_VALUE_UNION: UnionConfig = { - name: 'SessionInputAnswerValue', +const CHAT_INPUT_ANSWER_VALUE_UNION: UnionConfig = { + name: 'ChatInputAnswerValue', discriminantField: 'kind', doc: 'Value captured for one answer.', variants: [ - { variantName: 'Text', innerType: 'SessionInputTextAnswerValue', wireValue: 'text' }, - { variantName: 'Number', innerType: 'SessionInputNumberAnswerValue', wireValue: 'number' }, - { variantName: 'Boolean', innerType: 'SessionInputBooleanAnswerValue', wireValue: 'boolean' }, - { variantName: 'Selected', innerType: 'SessionInputSelectedAnswerValue', wireValue: 'selected' }, - { variantName: 'SelectedMany', innerType: 'SessionInputSelectedManyAnswerValue', wireValue: 'selected-many' }, + { variantName: 'Text', innerType: 'ChatInputTextAnswerValue', wireValue: 'text' }, + { variantName: 'Number', innerType: 'ChatInputNumberAnswerValue', wireValue: 'number' }, + { variantName: 'Boolean', innerType: 'ChatInputBooleanAnswerValue', wireValue: 'boolean' }, + { variantName: 'Selected', innerType: 'ChatInputSelectedAnswerValue', wireValue: 'selected' }, + { variantName: 'SelectedMany', innerType: 'ChatInputSelectedManyAnswerValue', wireValue: 'selected-many' }, ], unknown: true, }; -const SESSION_INPUT_ANSWER_UNION: UnionConfig = { - name: 'SessionInputAnswer', +const CHAT_INPUT_ANSWER_UNION: UnionConfig = { + name: 'ChatInputAnswer', discriminantField: 'state', doc: 'Draft, submitted, or skipped answer for one question.', variants: [ - { variantName: 'Draft', innerType: 'SessionInputAnswered', wireValue: 'draft' }, - { variantName: 'Submitted', innerType: 'SessionInputAnswered', wireValue: 'submitted' }, - { variantName: 'Skipped', innerType: 'SessionInputSkipped', wireValue: 'skipped' }, + { variantName: 'Draft', innerType: 'ChatInputAnswered', wireValue: 'draft' }, + { variantName: 'Submitted', innerType: 'ChatInputAnswered', wireValue: 'submitted' }, + { variantName: 'Skipped', innerType: 'ChatInputSkipped', wireValue: 'skipped' }, ], unknown: true, }; @@ -843,18 +849,53 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +function generateChatOrigin(): string { + return `/// How a chat came into existence. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum ChatOrigin { + /// Created directly by a user. + #[serde(rename = "user")] + User, + /// Forked from a specific turn of another chat. + #[serde(rename = "fork")] + Fork { + /// URI of the chat this one was forked from. + chat: Uri, + /// Turn the fork was taken from. + #[serde(rename = "turnId")] + turn_id: String, + }, + /// Spawned by a tool call in another chat. + #[serde(rename = "tool")] + Tool { + /// URI of the chat whose tool call spawned this one. + chat: Uri, + /// Tool call that spawned this chat. + #[serde(rename = "toolCallId")] + tool_call_id: String, + }, + /// 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), +}`; +} + function generateSnapshotState(): string { - return `/// The state payload of a snapshot — root, session, terminal, + return `/// The state payload of a snapshot — root, session, chat, terminal, /// changeset, resource-watch, or annotations state. /// /// Deserialized by trying session first (has required \`summary\`), then -/// terminal (has required \`content\`), then changeset (has required -/// \`status\` and \`files\`), then resource-watch (has required \`root\` and -/// \`recursive\`), then annotations (has required \`annotations\`), then root. +/// chat (has required \`turns\`), then terminal (has required \`content\`), +/// then changeset (has required \`status\` and \`files\`), then resource-watch +/// (has required \`root\` and \`recursive\`), then annotations (has required +/// \`annotations\`), then root. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum SnapshotState { Session(Box), + Chat(Box), Terminal(Box), Changeset(Box), ResourceWatch(Box), @@ -889,6 +930,8 @@ function generateStateFile(project: Project): string { } lines.push('// ─── Discriminated Unions ─────────────────────────────────────────────\n'); + lines.push(generateChatOrigin()); + lines.push(''); lines.push(generateDiscriminatedUnion(RESPONSE_PART_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_STATE_UNION)); @@ -897,11 +940,11 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TERMINAL_CONTENT_PART_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(SESSION_INPUT_QUESTION_UNION)); + lines.push(generateDiscriminatedUnion(CHAT_INPUT_QUESTION_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(SESSION_INPUT_ANSWER_VALUE_UNION)); + lines.push(generateDiscriminatedUnion(CHAT_INPUT_ANSWER_VALUE_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(SESSION_INPUT_ANSWER_UNION)); + lines.push(generateDiscriminatedUnion(CHAT_INPUT_ANSWER_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_RESULT_CONTENT_UNION)); lines.push(''); @@ -938,21 +981,26 @@ const ACTION_VARIANTS: { { type: 'root/configChanged', variantName: 'RootConfigChanged', tsInterface: 'RootConfigChangedAction' }, { type: 'session/ready', variantName: 'SessionReady', tsInterface: 'SessionReadyAction' }, { type: 'session/creationFailed', variantName: 'SessionCreationFailed', tsInterface: 'SessionCreationFailedAction' }, - { type: 'session/turnStarted', variantName: 'SessionTurnStarted', tsInterface: 'SessionTurnStartedAction' }, - { type: 'session/delta', variantName: 'SessionDelta', tsInterface: 'SessionDeltaAction' }, - { type: 'session/responsePart', variantName: 'SessionResponsePart', tsInterface: 'SessionResponsePartAction' }, - { type: 'session/toolCallStart', variantName: 'SessionToolCallStart', tsInterface: 'SessionToolCallStartAction' }, - { type: 'session/toolCallDelta', variantName: 'SessionToolCallDelta', tsInterface: 'SessionToolCallDeltaAction' }, - { type: 'session/toolCallReady', variantName: 'SessionToolCallReady', tsInterface: 'SessionToolCallReadyAction' }, - { type: 'session/toolCallConfirmed', variantName: 'SessionToolCallConfirmed', tsInterface: '_merged_' }, - { type: 'session/toolCallComplete', variantName: 'SessionToolCallComplete', tsInterface: 'SessionToolCallCompleteAction' }, - { type: 'session/toolCallResultConfirmed', variantName: 'SessionToolCallResultConfirmed', tsInterface: 'SessionToolCallResultConfirmedAction' }, - { type: 'session/turnComplete', variantName: 'SessionTurnComplete', tsInterface: 'SessionTurnCompleteAction' }, - { type: 'session/turnCancelled', variantName: 'SessionTurnCancelled', tsInterface: 'SessionTurnCancelledAction' }, - { type: 'session/error', variantName: 'SessionError', tsInterface: 'SessionErrorAction' }, + { type: 'session/chatAdded', variantName: 'SessionChatAdded', tsInterface: 'SessionChatAddedAction' }, + { type: 'session/chatRemoved', variantName: 'SessionChatRemoved', tsInterface: 'SessionChatRemovedAction' }, + { type: 'session/chatUpdated', variantName: 'SessionChatUpdated', tsInterface: 'SessionChatUpdatedAction' }, + { type: 'session/defaultChatChanged', variantName: 'SessionDefaultChatChanged', tsInterface: 'SessionDefaultChatChangedAction' }, + { type: 'chat/turnStarted', variantName: 'ChatTurnStarted', tsInterface: 'ChatTurnStartedAction' }, + { type: 'chat/delta', variantName: 'ChatDelta', tsInterface: 'ChatDeltaAction' }, + { type: 'chat/responsePart', variantName: 'ChatResponsePart', tsInterface: 'ChatResponsePartAction' }, + { type: 'chat/toolCallStart', variantName: 'ChatToolCallStart', tsInterface: 'ChatToolCallStartAction' }, + { type: 'chat/toolCallDelta', variantName: 'ChatToolCallDelta', tsInterface: 'ChatToolCallDeltaAction' }, + { type: 'chat/toolCallReady', variantName: 'ChatToolCallReady', tsInterface: 'ChatToolCallReadyAction' }, + { type: 'chat/toolCallConfirmed', variantName: 'ChatToolCallConfirmed', tsInterface: '_merged_chat_' }, + { type: 'chat/toolCallComplete', variantName: 'ChatToolCallComplete', tsInterface: 'ChatToolCallCompleteAction' }, + { type: 'chat/toolCallResultConfirmed', variantName: 'ChatToolCallResultConfirmed', tsInterface: 'ChatToolCallResultConfirmedAction' }, + { type: 'chat/toolCallContentChanged', variantName: 'ChatToolCallContentChanged', tsInterface: 'ChatToolCallContentChangedAction' }, + { type: 'chat/turnComplete', variantName: 'ChatTurnComplete', tsInterface: 'ChatTurnCompleteAction' }, + { type: 'chat/turnCancelled', variantName: 'ChatTurnCancelled', tsInterface: 'ChatTurnCancelledAction' }, + { type: 'chat/error', variantName: 'ChatError', tsInterface: 'ChatErrorAction' }, { type: 'session/titleChanged', variantName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, - { type: 'session/usage', variantName: 'SessionUsage', tsInterface: 'SessionUsageAction' }, - { type: 'session/reasoning', variantName: 'SessionReasoning', tsInterface: 'SessionReasoningAction' }, + { type: 'chat/usage', variantName: 'ChatUsage', tsInterface: 'ChatUsageAction' }, + { type: 'chat/reasoning', variantName: 'ChatReasoning', tsInterface: 'ChatReasoningAction' }, { type: 'session/modelChanged', variantName: 'SessionModelChanged', tsInterface: 'SessionModelChangedAction' }, { type: 'session/agentChanged', variantName: 'SessionAgentChanged', tsInterface: 'SessionAgentChangedAction' }, { type: 'session/isReadChanged', variantName: 'SessionIsReadChanged', tsInterface: 'SessionIsReadChangedAction' }, @@ -962,21 +1010,20 @@ const ACTION_VARIANTS: { { type: 'session/serverToolsChanged', variantName: 'SessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientChanged', variantName: 'SessionActiveClientChanged', tsInterface: 'SessionActiveClientChangedAction' }, { type: 'session/activeClientToolsChanged', variantName: 'SessionActiveClientToolsChanged', tsInterface: 'SessionActiveClientToolsChangedAction' }, - { type: 'session/pendingMessageSet', variantName: 'SessionPendingMessageSet', tsInterface: 'SessionPendingMessageSetAction' }, - { type: 'session/pendingMessageRemoved', variantName: 'SessionPendingMessageRemoved', tsInterface: 'SessionPendingMessageRemovedAction' }, - { type: 'session/queuedMessagesReordered', variantName: 'SessionQueuedMessagesReordered', tsInterface: 'SessionQueuedMessagesReorderedAction' }, - { type: 'session/inputRequested', variantName: 'SessionInputRequested', tsInterface: 'SessionInputRequestedAction' }, - { type: 'session/inputAnswerChanged', variantName: 'SessionInputAnswerChanged', tsInterface: 'SessionInputAnswerChangedAction' }, - { type: 'session/inputCompleted', variantName: 'SessionInputCompleted', tsInterface: 'SessionInputCompletedAction' }, + { type: 'chat/pendingMessageSet', variantName: 'ChatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, + { type: 'chat/pendingMessageRemoved', variantName: 'ChatPendingMessageRemoved', tsInterface: 'ChatPendingMessageRemovedAction' }, + { type: 'chat/queuedMessagesReordered', variantName: 'ChatQueuedMessagesReordered', tsInterface: 'ChatQueuedMessagesReorderedAction' }, + { type: 'chat/inputRequested', variantName: 'ChatInputRequested', tsInterface: 'ChatInputRequestedAction' }, + { type: 'chat/inputAnswerChanged', variantName: 'ChatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, + { type: 'chat/inputCompleted', variantName: 'ChatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, { type: 'session/customizationsChanged', variantName: 'SessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', variantName: 'SessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', variantName: 'SessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction', boxed: true }, { type: 'session/customizationRemoved', variantName: 'SessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, { type: 'session/mcpServerStateChanged', variantName: 'SessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction', boxed: true }, - { type: 'session/truncated', variantName: 'SessionTruncated', tsInterface: 'SessionTruncatedAction' }, + { type: 'chat/truncated', variantName: 'ChatTruncated', tsInterface: 'ChatTruncatedAction' }, { type: 'session/configChanged', variantName: 'SessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', variantName: 'SessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, - { type: 'session/toolCallContentChanged', variantName: 'SessionToolCallContentChanged', tsInterface: 'SessionToolCallContentChangedAction' }, { type: 'changeset/statusChanged', variantName: 'ChangesetStatusChanged', tsInterface: 'ChangesetStatusChangedAction' }, { type: 'changeset/fileSet', variantName: 'ChangesetFileSet', tsInterface: 'ChangesetFileSetAction' }, { type: 'changeset/fileRemoved', variantName: 'ChangesetFileRemoved', tsInterface: 'ChangesetFileRemovedAction' }, @@ -1003,11 +1050,11 @@ const ACTION_VARIANTS: { { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, ]; -function generateMergedToolCallConfirmedStruct(): string { +function generateMergedToolCallConfirmedStruct(scope: 'Session' | 'Chat' = 'Session'): string { return `/// Client approves or denies a pending tool call (merged approved + denied variants). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SessionToolCallConfirmedAction { +pub struct ${scope}ToolCallConfirmedAction { pub turn_id: String, pub tool_call_id: String, /// Additional provider-specific metadata for this tool call. @@ -1038,7 +1085,7 @@ pub struct SessionToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputAnswer, SessionInputRequest, SessionInputResponseKind, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, 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(''); // ActionType enum @@ -1076,9 +1123,10 @@ pub struct ActionEnvelope { // Individual action structs (as variant inner types — omit the `type` field) lines.push('// ─── Action Payloads ─────────────────────────────────────────────────\n'); + const priorPartials = new Set(requiredPartialStructs); for (const v of ACTION_VARIANTS) { - if (v.tsInterface === '_merged_') { - lines.push(generateMergedToolCallConfirmedStruct()); + if (v.tsInterface === '_merged_' || v.tsInterface === '_merged_chat_') { + lines.push(generateMergedToolCallConfirmedStruct(v.tsInterface === '_merged_chat_' ? 'Chat' : 'Session')); lines.push(''); continue; } @@ -1093,11 +1141,31 @@ pub struct ActionEnvelope { } } + // Emit any Partial structs referenced by action payloads (e.g. Partial + // on SessionChatUpdatedAction). Mirrors the notification-side emission. + const newPartials = [...requiredPartialStructs].filter(n => !priorPartials.has(n)); + if (newPartials.length > 0) { + lines.push('// ─── Partial Summaries ────────────────────────────────────────────────\n'); + for (const tsName of newPartials) { + try { + lines.push(generatePartialStruct(project, tsName)); + lines.push(''); + } catch (e) { + lines.push(`// TODO: could not generate Partial<${tsName}>: ${e}`); + lines.push(''); + } + } + } + // StateAction union lines.push('// ─── StateAction Union ───────────────────────────────────────────────\n'); const variants: UnionVariant[] = ACTION_VARIANTS.map(v => ({ variantName: v.variantName, - innerType: v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : stripIPrefix(v.tsInterface), + innerType: v.tsInterface === '_merged_' + ? 'SessionToolCallConfirmedAction' + : v.tsInterface === '_merged_chat_' + ? 'ChatToolCallConfirmedAction' + : stripIPrefix(v.tsInterface), wireValue: v.type, boxed: v.boxed, })); @@ -1126,6 +1194,8 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'SubscribeParams' }, { name: 'SubscribeResult' }, { name: 'SessionForkSource' }, { name: 'CreateSessionParams' }, { name: 'DisposeSessionParams' }, + { name: 'ChatForkSource' }, { name: 'CreateChatParams' }, + { name: 'DisposeChatParams' }, { name: 'ListSessionsParams' }, { name: 'ListSessionsResult' }, { name: 'ResourceReadParams' }, { name: 'ResourceReadResult' }, { name: 'ResourceWriteParams' }, { name: 'ResourceWriteResult' }, @@ -1165,7 +1235,7 @@ function generateCommandsFile(project: Project): string { lines.push('#[allow(unused_imports)]'); lines.push('use crate::actions::{ActionEnvelope, StateAction};'); lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentSelection, ContentRef, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); + lines.push('use crate::state::{AgentSelection, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); @@ -1524,12 +1594,17 @@ function checkExhaustiveness(project: Project): void { 'SessionToolCallApprovedAction', 'SessionToolCallDeniedAction', 'SessionToolCallConfirmedAction', + 'ChatToolCallApprovedAction', // merged into ChatToolCallConfirmedAction + 'ChatToolCallDeniedAction', // merged into ChatToolCallConfirmedAction + 'ChatToolCallConfirmedAction', // emitted as merged variant + 'ChatAction', // source-only union covered by StateAction + 'ChatOrigin', // hand-generated union for inline variants 'PingParams', 'TerminalClaim', 'TerminalContentPart', - 'SessionInputQuestion', - 'SessionInputAnswerValue', - 'SessionInputAnswer', + 'ChatInputQuestion', + 'ChatInputAnswerValue', + 'ChatInputAnswer', 'MessageAttachment', 'MessageAttachmentBase', 'Customization', // CUSTOMIZATION_UNION discriminated union diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 96c8bdf2..ed0f6030 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -108,7 +108,11 @@ function mapType(tsType: string, propName?: string, containerName?: string): str || tsType === 'RootState | SessionState | TerminalState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' - || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState') return 'SnapshotState'; + || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' + || tsType === 'RootState | SessionState | ChatState' + || tsType === 'RootState | SessionState | ChatState | TerminalState' + || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' + || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState') return 'SnapshotState'; // T | null → T? const nullMatch = tsType.match(/^(.+?)\s*\|\s*null$/); @@ -507,8 +511,8 @@ function generatePartialStructFromInterface( const STATE_ENUMS = [ 'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus', - 'SessionInputAnswerState', 'SessionInputAnswerValueKind', 'SessionInputQuestionKind', - 'SessionInputResponseKind', + 'ChatOriginKind', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', + 'ChatInputResponseKind', 'TurnState', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', @@ -520,17 +524,17 @@ const STATE_ENUMS = [ const STATE_STRUCTS = [ 'Icon', 'ProtectedResourceMetadata', 'RootState', 'RootConfigState', 'AgentInfo', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', - 'PendingMessage', 'SessionState', 'SessionActiveClient', + 'PendingMessage', 'ChatState', 'ChatSummary', 'SessionState', 'SessionActiveClient', 'SessionSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', - 'SessionInputOption', - 'SessionInputTextAnswerValue', 'SessionInputNumberAnswerValue', - 'SessionInputBooleanAnswerValue', 'SessionInputSelectedAnswerValue', - 'SessionInputSelectedManyAnswerValue', 'SessionInputAnswered', - 'SessionInputSkipped', - 'SessionInputTextQuestion', - 'SessionInputNumberQuestion', 'SessionInputBooleanQuestion', - 'SessionInputSingleSelectQuestion', 'SessionInputMultiSelectQuestion', - 'SessionInputRequest', + 'ChatInputOption', + 'ChatInputTextAnswerValue', 'ChatInputNumberAnswerValue', + 'ChatInputBooleanAnswerValue', 'ChatInputSelectedAnswerValue', + 'ChatInputSelectedManyAnswerValue', 'ChatInputAnswered', + 'ChatInputSkipped', + 'ChatInputTextQuestion', + 'ChatInputNumberQuestion', 'ChatInputBooleanQuestion', + 'ChatInputSingleSelectQuestion', 'ChatInputMultiSelectQuestion', + 'ChatInputRequest', 'TextPosition', 'TextRange', 'TextSelection', 'SimpleMessageAttachment', 'MessageEmbeddedResourceAttachment', 'MessageResourceAttachment', 'MessageAnnotationsAttachment', @@ -607,37 +611,37 @@ const TERMINAL_CONTENT_PART_UNION: UnionConfig = { }; const SESSION_INPUT_QUESTION_UNION: UnionConfig = { - name: 'SessionInputQuestion', + name: 'ChatInputQuestion', discriminantField: 'kind', variants: [ - { caseName: 'text', structName: 'SessionInputTextQuestion', discriminantValue: 'text' }, - { caseName: 'number', structName: 'SessionInputNumberQuestion', discriminantValue: 'number' }, - { caseName: 'integer', structName: 'SessionInputNumberQuestion', discriminantValue: 'integer' }, - { caseName: 'boolean', structName: 'SessionInputBooleanQuestion', discriminantValue: 'boolean' }, - { caseName: 'singleSelect', structName: 'SessionInputSingleSelectQuestion', discriminantValue: 'single-select' }, - { caseName: 'multiSelect', structName: 'SessionInputMultiSelectQuestion', discriminantValue: 'multi-select' }, + { caseName: 'text', structName: 'ChatInputTextQuestion', discriminantValue: 'text' }, + { caseName: 'number', structName: 'ChatInputNumberQuestion', discriminantValue: 'number' }, + { caseName: 'integer', structName: 'ChatInputNumberQuestion', discriminantValue: 'integer' }, + { caseName: 'boolean', structName: 'ChatInputBooleanQuestion', discriminantValue: 'boolean' }, + { caseName: 'singleSelect', structName: 'ChatInputSingleSelectQuestion', discriminantValue: 'single-select' }, + { caseName: 'multiSelect', structName: 'ChatInputMultiSelectQuestion', discriminantValue: 'multi-select' }, ], }; const SESSION_INPUT_ANSWER_VALUE_UNION: UnionConfig = { - name: 'SessionInputAnswerValue', + name: 'ChatInputAnswerValue', discriminantField: 'kind', variants: [ - { caseName: 'text', structName: 'SessionInputTextAnswerValue', discriminantValue: 'text' }, - { caseName: 'number', structName: 'SessionInputNumberAnswerValue', discriminantValue: 'number' }, - { caseName: 'boolean', structName: 'SessionInputBooleanAnswerValue', discriminantValue: 'boolean' }, - { caseName: 'selected', structName: 'SessionInputSelectedAnswerValue', discriminantValue: 'selected' }, - { caseName: 'selectedMany', structName: 'SessionInputSelectedManyAnswerValue', discriminantValue: 'selected-many' }, + { caseName: 'text', structName: 'ChatInputTextAnswerValue', discriminantValue: 'text' }, + { caseName: 'number', structName: 'ChatInputNumberAnswerValue', discriminantValue: 'number' }, + { caseName: 'boolean', structName: 'ChatInputBooleanAnswerValue', discriminantValue: 'boolean' }, + { caseName: 'selected', structName: 'ChatInputSelectedAnswerValue', discriminantValue: 'selected' }, + { caseName: 'selectedMany', structName: 'ChatInputSelectedManyAnswerValue', discriminantValue: 'selected-many' }, ], }; const SESSION_INPUT_ANSWER_UNION: UnionConfig = { - name: 'SessionInputAnswer', + name: 'ChatInputAnswer', discriminantField: 'state', variants: [ - { caseName: 'draft', structName: 'SessionInputAnswered', discriminantValue: 'draft' }, - { caseName: 'submitted', structName: 'SessionInputAnswered', discriminantValue: 'submitted' }, - { caseName: 'skipped', structName: 'SessionInputSkipped', discriminantValue: 'skipped' }, + { caseName: 'draft', structName: 'ChatInputAnswered', discriminantValue: 'draft' }, + { caseName: 'submitted', structName: 'ChatInputAnswered', discriminantValue: 'submitted' }, + { caseName: 'skipped', structName: 'ChatInputSkipped', discriminantValue: 'skipped' }, ], }; @@ -796,10 +800,11 @@ public enum StringOrMarkdown: Codable, Sendable, Equatable { } function generateSnapshotState(): string { - return `/// The state payload of a snapshot — root, session, terminal, changeset, resource-watch, or annotations state. + return `/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, or annotations state. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) + case chat(ChatState) case terminal(TerminalState) case changeset(ChangesetState) case resourceWatch(ResourceWatchState) @@ -826,6 +831,7 @@ public enum SnapshotState: Codable, Sendable { switch self { case .root(let state): try state.encode(to: encoder) case .session(let state): try state.encode(to: encoder) + case .chat(let state): try state.encode(to: encoder) case .terminal(let state): try state.encode(to: encoder) case .changeset(let state): try state.encode(to: encoder) case .resourceWatch(let state): try state.encode(to: encoder) @@ -835,6 +841,69 @@ public enum SnapshotState: Codable, Sendable { }`; } + +function generateChatOriginSwift(): string { + return `public struct ChatOriginUser: Codable, Sendable { + public var kind: ChatOriginKind + + public init(kind: ChatOriginKind = .user) { + self.kind = kind + } +} + +public struct ChatOriginFork: Codable, Sendable { + public var kind: ChatOriginKind + public var chat: String + public var turnId: String + + public init(kind: ChatOriginKind = .fork, chat: String, turnId: String) { + self.kind = kind + self.chat = chat + self.turnId = turnId + } +} + +public struct ChatOriginTool: Codable, Sendable { + public var kind: ChatOriginKind + public var chat: String + public var toolCallId: String + + public init(kind: ChatOriginKind = .tool, chat: String, toolCallId: String) { + self.kind = kind + self.chat = chat + self.toolCallId = toolCallId + } +} + +public enum ChatOrigin: Codable, Sendable { + case user(ChatOriginUser) + case fork(ChatOriginFork) + case tool(ChatOriginTool) + + private enum DiscriminatorCodingKeys: String, CodingKey { case kind } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminatorCodingKeys.self) + let discriminant = try container.decode(String.self, forKey: .kind) + switch discriminant { + case "user": self = .user(try ChatOriginUser(from: decoder)) + case "fork": self = .fork(try ChatOriginFork(from: decoder)) + case "tool": self = .tool(try ChatOriginTool(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown ChatOrigin kind: \\(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .user(let value): try value.encode(to: encoder) + case .fork(let value): try value.encode(to: encoder) + case .tool(let value): try value.encode(to: encoder) + } + } +}`; +} + function generateStateFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; @@ -866,6 +935,8 @@ function generateStateFile(project: Project): string { } lines.push('// MARK: - Discriminated Unions\n'); + lines.push(generateChatOriginSwift()); + lines.push(''); lines.push(generateDiscriminatedUnion(RESPONSE_PART_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_STATE_UNION)); @@ -908,21 +979,26 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'root/activeSessionsChanged', caseName: 'rootActiveSessionsChanged', tsInterface: 'RootActiveSessionsChangedAction' }, { type: 'session/ready', caseName: 'sessionReady', tsInterface: 'SessionReadyAction' }, { type: 'session/creationFailed', caseName: 'sessionCreationFailed', tsInterface: 'SessionCreationFailedAction' }, - { type: 'session/turnStarted', caseName: 'sessionTurnStarted', tsInterface: 'SessionTurnStartedAction' }, - { type: 'session/delta', caseName: 'sessionDelta', tsInterface: 'SessionDeltaAction' }, - { type: 'session/responsePart', caseName: 'sessionResponsePart', tsInterface: 'SessionResponsePartAction' }, - { type: 'session/toolCallStart', caseName: 'sessionToolCallStart', tsInterface: 'SessionToolCallStartAction' }, - { type: 'session/toolCallDelta', caseName: 'sessionToolCallDelta', tsInterface: 'SessionToolCallDeltaAction' }, - { type: 'session/toolCallReady', caseName: 'sessionToolCallReady', tsInterface: 'SessionToolCallReadyAction' }, - { type: 'session/toolCallConfirmed', caseName: 'sessionToolCallConfirmed', tsInterface: '_merged_' }, - { type: 'session/toolCallComplete', caseName: 'sessionToolCallComplete', tsInterface: 'SessionToolCallCompleteAction' }, - { type: 'session/toolCallResultConfirmed', caseName: 'sessionToolCallResultConfirmed', tsInterface: 'SessionToolCallResultConfirmedAction' }, - { type: 'session/turnComplete', caseName: 'sessionTurnComplete', tsInterface: 'SessionTurnCompleteAction' }, - { type: 'session/turnCancelled', caseName: 'sessionTurnCancelled', tsInterface: 'SessionTurnCancelledAction' }, - { type: 'session/error', caseName: 'sessionError', tsInterface: 'SessionErrorAction' }, + { type: 'session/chatAdded', caseName: 'sessionChatAdded', tsInterface: 'SessionChatAddedAction' }, + { type: 'session/chatRemoved', caseName: 'sessionChatRemoved', tsInterface: 'SessionChatRemovedAction' }, + { type: 'session/chatUpdated', caseName: 'sessionChatUpdated', tsInterface: 'SessionChatUpdatedAction' }, + { type: 'session/defaultChatChanged', caseName: 'sessionDefaultChatChanged', tsInterface: 'SessionDefaultChatChangedAction' }, + { type: 'chat/turnStarted', caseName: 'chatTurnStarted', tsInterface: 'ChatTurnStartedAction' }, + { type: 'chat/delta', caseName: 'chatDelta', tsInterface: 'ChatDeltaAction' }, + { type: 'chat/responsePart', caseName: 'chatResponsePart', tsInterface: 'ChatResponsePartAction' }, + { type: 'chat/toolCallStart', caseName: 'chatToolCallStart', tsInterface: 'ChatToolCallStartAction' }, + { type: 'chat/toolCallDelta', caseName: 'chatToolCallDelta', tsInterface: 'ChatToolCallDeltaAction' }, + { type: 'chat/toolCallReady', caseName: 'chatToolCallReady', tsInterface: 'ChatToolCallReadyAction' }, + { type: 'chat/toolCallConfirmed', caseName: 'chatToolCallConfirmed', tsInterface: '_merged_chat_' }, + { type: 'chat/toolCallComplete', caseName: 'chatToolCallComplete', tsInterface: 'ChatToolCallCompleteAction' }, + { type: 'chat/toolCallResultConfirmed', caseName: 'chatToolCallResultConfirmed', tsInterface: 'ChatToolCallResultConfirmedAction' }, + { type: 'chat/toolCallContentChanged', caseName: 'chatToolCallContentChanged', tsInterface: 'ChatToolCallContentChangedAction' }, + { type: 'chat/turnComplete', caseName: 'chatTurnComplete', tsInterface: 'ChatTurnCompleteAction' }, + { type: 'chat/turnCancelled', caseName: 'chatTurnCancelled', tsInterface: 'ChatTurnCancelledAction' }, + { type: 'chat/error', caseName: 'chatError', tsInterface: 'ChatErrorAction' }, { type: 'session/titleChanged', caseName: 'sessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, - { type: 'session/usage', caseName: 'sessionUsage', tsInterface: 'SessionUsageAction' }, - { type: 'session/reasoning', caseName: 'sessionReasoning', tsInterface: 'SessionReasoningAction' }, + { type: 'chat/usage', caseName: 'chatUsage', tsInterface: 'ChatUsageAction' }, + { type: 'chat/reasoning', caseName: 'chatReasoning', tsInterface: 'ChatReasoningAction' }, { type: 'session/modelChanged', caseName: 'sessionModelChanged', tsInterface: 'SessionModelChangedAction' }, { type: 'session/agentChanged', caseName: 'sessionAgentChanged', tsInterface: 'SessionAgentChangedAction' }, { type: 'session/isReadChanged', caseName: 'sessionIsReadChanged', tsInterface: 'SessionIsReadChangedAction' }, @@ -932,21 +1008,20 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'session/serverToolsChanged', caseName: 'sessionServerToolsChanged', tsInterface: 'SessionServerToolsChangedAction' }, { type: 'session/activeClientChanged', caseName: 'sessionActiveClientChanged', tsInterface: 'SessionActiveClientChangedAction' }, { type: 'session/activeClientToolsChanged', caseName: 'sessionActiveClientToolsChanged', tsInterface: 'SessionActiveClientToolsChangedAction' }, - { type: 'session/pendingMessageSet', caseName: 'sessionPendingMessageSet', tsInterface: 'SessionPendingMessageSetAction' }, - { type: 'session/pendingMessageRemoved', caseName: 'sessionPendingMessageRemoved', tsInterface: 'SessionPendingMessageRemovedAction' }, - { type: 'session/queuedMessagesReordered', caseName: 'sessionQueuedMessagesReordered', tsInterface: 'SessionQueuedMessagesReorderedAction' }, - { type: 'session/inputRequested', caseName: 'sessionInputRequested', tsInterface: 'SessionInputRequestedAction' }, - { type: 'session/inputAnswerChanged', caseName: 'sessionInputAnswerChanged', tsInterface: 'SessionInputAnswerChangedAction' }, - { type: 'session/inputCompleted', caseName: 'sessionInputCompleted', tsInterface: 'SessionInputCompletedAction' }, + { type: 'chat/pendingMessageSet', caseName: 'chatPendingMessageSet', tsInterface: 'ChatPendingMessageSetAction' }, + { type: 'chat/pendingMessageRemoved', caseName: 'chatPendingMessageRemoved', tsInterface: 'ChatPendingMessageRemovedAction' }, + { type: 'chat/queuedMessagesReordered', caseName: 'chatQueuedMessagesReordered', tsInterface: 'ChatQueuedMessagesReorderedAction' }, + { type: 'chat/inputRequested', caseName: 'chatInputRequested', tsInterface: 'ChatInputRequestedAction' }, + { type: 'chat/inputAnswerChanged', caseName: 'chatInputAnswerChanged', tsInterface: 'ChatInputAnswerChangedAction' }, + { type: 'chat/inputCompleted', caseName: 'chatInputCompleted', tsInterface: 'ChatInputCompletedAction' }, { type: 'session/customizationsChanged', caseName: 'sessionCustomizationsChanged', tsInterface: 'SessionCustomizationsChangedAction' }, { type: 'session/customizationToggled', caseName: 'sessionCustomizationToggled', tsInterface: 'SessionCustomizationToggledAction' }, { type: 'session/customizationUpdated', caseName: 'sessionCustomizationUpdated', tsInterface: 'SessionCustomizationUpdatedAction' }, { type: 'session/customizationRemoved', caseName: 'sessionCustomizationRemoved', tsInterface: 'SessionCustomizationRemovedAction' }, - { type: 'session/mcpServerStateChanged', caseName: 'sessionMcpServerStatusChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, - { type: 'session/truncated', caseName: 'sessionTruncated', tsInterface: 'SessionTruncatedAction' }, + { type: 'session/mcpServerStateChanged', caseName: 'sessionMcpServerStateChanged', tsInterface: 'SessionMcpServerStateChangedAction' }, + { type: 'chat/truncated', caseName: 'chatTruncated', tsInterface: 'ChatTruncatedAction' }, { type: 'session/configChanged', caseName: 'sessionConfigChanged', tsInterface: 'SessionConfigChangedAction' }, { type: 'session/metaChanged', caseName: 'sessionMetaChanged', tsInterface: 'SessionMetaChangedAction' }, - { type: 'session/toolCallContentChanged', caseName: 'sessionToolCallContentChanged', tsInterface: 'SessionToolCallContentChangedAction' }, { type: 'changeset/statusChanged', caseName: 'changesetStatusChanged', tsInterface: 'ChangesetStatusChangedAction' }, { type: 'changeset/fileSet', caseName: 'changesetFileSet', tsInterface: 'ChangesetFileSetAction' }, { type: 'changeset/fileRemoved', caseName: 'changesetFileRemoved', tsInterface: 'ChangesetFileRemovedAction' }, @@ -975,9 +1050,11 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] ]; /** Merged struct for the approved/denied tool call confirmed action */ -function generateMergedToolCallConfirmedStruct(): string { +function generateMergedToolCallConfirmedStruct(scope: 'Session' | 'Chat' = 'Session'): string { + const className = `${scope}ToolCallConfirmedAction`; + const wireType = scope === 'Chat' ? 'chat/toolCallConfirmed' : 'session/toolCallConfirmed'; return `/// Client approves or denies a pending tool call (merged approved + denied variants). -public struct SessionToolCallConfirmedAction: Codable, Sendable { +public struct ${className}: Codable, Sendable { /// Action type discriminant public var type: String /// Turn identifier @@ -1007,7 +1084,7 @@ public struct SessionToolCallConfirmedAction: Codable, Sendable { } public init( - type: String = "session/toolCallConfirmed", + type: String = "${wireType}", turnId: String, toolCallId: String, approved: Bool, @@ -1054,9 +1131,10 @@ function generateActionsFile(project: Project): string { // Individual action structs lines.push('// MARK: - Action Types\n'); + const priorPartialsAction = new Set(requiredPartialStructs); for (const variant of ACTION_VARIANTS) { - if (variant.tsInterface === '_merged_') { - lines.push(generateMergedToolCallConfirmedStruct()); + if (variant.tsInterface === '_merged_' || variant.tsInterface === '_merged_chat_') { + lines.push(generateMergedToolCallConfirmedStruct(variant.tsInterface === '_merged_chat_' ? 'Chat' : 'Session')); lines.push(''); continue; } @@ -1069,12 +1147,29 @@ function generateActionsFile(project: Project): string { } } + // Emit any Partial types referenced only by action payloads (e.g. + // Partial on SessionChatUpdatedAction). Mirrors the + // notification-side emission. + const actionNewPartials = [...requiredPartialStructs].filter(n => !priorPartialsAction.has(n)); + if (actionNewPartials.length > 0) { + lines.push('// MARK: - Partial Summary Types\n'); + for (const tsName of actionNewPartials) { + try { + lines.push(generatePartialStructFromInterface(project, tsName)); + lines.push(''); + } catch (e) { + lines.push(`// TODO: Could not generate Partial<${tsName}>: ${e}`); + lines.push(''); + } + } + } + // StateAction discriminated union lines.push('// MARK: - StateAction Union\n'); lines.push('/// Discriminated union of all state actions.'); lines.push('public enum StateAction: Codable, Sendable {'); for (const v of ACTION_VARIANTS) { - lines.push(` case ${v.caseName}(${v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface})`); + lines.push(` case ${v.caseName}(${v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' : v.tsInterface === '_merged_chat_' ? 'ChatToolCallConfirmedAction' : v.tsInterface})`); } lines.push(' /// Unknown or future action type; reducers treat this as a no-op.'); lines.push(' case unknown(type: String)'); @@ -1088,7 +1183,9 @@ function generateActionsFile(project: Project): string { for (const v of ACTION_VARIANTS) { const structName = v.tsInterface === '_merged_' ? 'SessionToolCallConfirmedAction' - : v.tsInterface; + : v.tsInterface === '_merged_chat_' + ? 'ChatToolCallConfirmedAction' + : v.tsInterface; lines.push(` case ${JSON.stringify(v.type)}:`); lines.push(` self = .${v.caseName}(try ${structName}(from: decoder))`); } @@ -1120,6 +1217,7 @@ const COMMAND_STRUCTS = [ 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', + 'ChatForkSource', 'CreateChatParams', 'DisposeChatParams', 'ListSessionsParams', 'ListSessionsResult', 'ResourceReadParams', 'ResourceReadResult', 'ResourceWriteParams', 'ResourceWriteResult', @@ -1659,9 +1757,14 @@ function checkExhaustiveness(project: Project): void { 'PingParams', // empty interface; no Swift type emitted 'TerminalClaim', // TERMINAL_CLAIM_UNION discriminated union 'TerminalContentPart', // TERMINAL_CONTENT_PART_UNION discriminated union - 'SessionInputQuestion', // SESSION_INPUT_QUESTION_UNION discriminated union - 'SessionInputAnswerValue', // SESSION_INPUT_ANSWER_VALUE_UNION discriminated union - 'SessionInputAnswer', // SESSION_INPUT_ANSWER_UNION discriminated union + 'ChatInputQuestion', // SESSION_INPUT_QUESTION_UNION discriminated union + 'ChatInputAnswerValue', // SESSION_INPUT_ANSWER_VALUE_UNION discriminated union + 'ChatInputAnswer', // CHAT_INPUT_ANSWER_UNION discriminated union + 'ChatOrigin', // hand-generated union for inline variants + 'ChatToolCallApprovedAction', + 'ChatToolCallDeniedAction', + 'ChatToolCallConfirmedAction', + 'ChatAction', 'MessageAttachment', // MESSAGE_ATTACHMENT_UNION discriminated union 'MessageAttachmentBase', // base interface, flattened into the variant structs via `extends` 'Customization', // CUSTOMIZATION_UNION discriminated union diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 923d5f9b..d9c476ca 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -9,45 +9,49 @@ import type { RootConfigChangedAction, SessionReadyAction, SessionCreationFailedAction, - SessionTurnStartedAction, - SessionDeltaAction, - SessionResponsePartAction, - SessionToolCallStartAction, - SessionToolCallDeltaAction, - SessionToolCallReadyAction, - SessionToolCallConfirmedAction, - SessionToolCallCompleteAction, - SessionToolCallResultConfirmedAction, - SessionToolCallContentChangedAction, - SessionTurnCompleteAction, - SessionTurnCancelledAction, - SessionErrorAction, + SessionChatAddedAction, + SessionChatRemovedAction, + SessionChatUpdatedAction, + SessionDefaultChatChangedAction, SessionTitleChangedAction, - SessionUsageAction, - SessionReasoningAction, SessionModelChangedAction, SessionAgentChangedAction, SessionServerToolsChangedAction, SessionActiveClientChangedAction, SessionActiveClientToolsChangedAction, - SessionPendingMessageSetAction, - SessionPendingMessageRemovedAction, - SessionQueuedMessagesReorderedAction, - SessionInputRequestedAction, - SessionInputAnswerChangedAction, - SessionInputCompletedAction, SessionCustomizationsChangedAction, SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, SessionMcpServerStateChangedAction, - SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, SessionActivityChangedAction, SessionChangesetsChangedAction, SessionConfigChangedAction, SessionMetaChangedAction, + ChatTurnStartedAction, + ChatDeltaAction, + ChatResponsePartAction, + ChatToolCallStartAction, + ChatToolCallDeltaAction, + ChatToolCallReadyAction, + ChatToolCallConfirmedAction, + ChatToolCallCompleteAction, + ChatToolCallResultConfirmedAction, + ChatToolCallContentChangedAction, + ChatTurnCompleteAction, + ChatTurnCancelledAction, + ChatErrorAction, + ChatUsageAction, + ChatReasoningAction, + ChatPendingMessageSetAction, + ChatPendingMessageRemovedAction, + ChatQueuedMessagesReorderedAction, + ChatInputRequestedAction, + ChatInputAnswerChangedAction, + ChatInputCompletedAction, + ChatTruncatedAction, ChangesetStatusChangedAction, ChangesetFileSetAction, ChangesetFileRemovedAction, @@ -75,7 +79,7 @@ import type { import { ActionType } from './actions.js'; -// ─── Root vs Session vs Terminal vs Changeset Action Unions ───────────────── +// ─── Root vs Session vs Chat vs Terminal vs Changeset Action Unions ───────────────── /** Union of all root-scoped actions. */ export type RootAction = @@ -101,39 +105,21 @@ export type ServerRootAction = export type SessionAction = | SessionReadyAction | SessionCreationFailedAction - | SessionTurnStartedAction - | SessionDeltaAction - | SessionResponsePartAction - | SessionToolCallStartAction - | SessionToolCallDeltaAction - | SessionToolCallReadyAction - | SessionToolCallConfirmedAction - | SessionToolCallCompleteAction - | SessionToolCallResultConfirmedAction - | SessionToolCallContentChangedAction - | SessionTurnCompleteAction - | SessionTurnCancelledAction - | SessionErrorAction + | SessionChatAddedAction + | SessionChatRemovedAction + | SessionChatUpdatedAction + | SessionDefaultChatChangedAction | SessionTitleChangedAction - | SessionUsageAction - | SessionReasoningAction | SessionModelChangedAction | SessionAgentChangedAction | SessionServerToolsChangedAction | SessionActiveClientChangedAction | SessionActiveClientToolsChangedAction - | SessionPendingMessageSetAction - | SessionPendingMessageRemovedAction - | SessionQueuedMessagesReorderedAction - | SessionInputRequestedAction - | SessionInputAnswerChangedAction - | SessionInputCompletedAction | SessionCustomizationsChangedAction | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction | SessionMcpServerStateChangedAction - | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction | SessionActivityChangedAction @@ -144,24 +130,12 @@ export type SessionAction = /** Union of session actions that clients may dispatch. */ export type ClientSessionAction = - | SessionTurnStartedAction - | SessionToolCallConfirmedAction - | SessionToolCallCompleteAction - | SessionToolCallResultConfirmedAction - | SessionToolCallContentChangedAction - | SessionTurnCancelledAction | SessionTitleChangedAction | SessionModelChangedAction | SessionAgentChangedAction | SessionActiveClientChangedAction | SessionActiveClientToolsChangedAction - | SessionPendingMessageSetAction - | SessionPendingMessageRemovedAction - | SessionQueuedMessagesReorderedAction - | SessionInputAnswerChangedAction - | SessionInputCompletedAction | SessionCustomizationToggledAction - | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction | SessionConfigChangedAction @@ -171,17 +145,11 @@ export type ClientSessionAction = export type ServerSessionAction = | SessionReadyAction | SessionCreationFailedAction - | SessionDeltaAction - | SessionResponsePartAction - | SessionToolCallStartAction - | SessionToolCallDeltaAction - | SessionToolCallReadyAction - | SessionTurnCompleteAction - | SessionErrorAction - | SessionUsageAction - | SessionReasoningAction + | SessionChatAddedAction + | SessionChatRemovedAction + | SessionChatUpdatedAction + | SessionDefaultChatChangedAction | SessionServerToolsChangedAction - | SessionInputRequestedAction | SessionCustomizationsChangedAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction @@ -191,6 +159,62 @@ export type ServerSessionAction = | SessionMetaChangedAction ; +/** Union of all chat-scoped actions. */ +export type ChatAction = + | ChatTurnStartedAction + | ChatDeltaAction + | ChatResponsePartAction + | ChatToolCallStartAction + | ChatToolCallDeltaAction + | ChatToolCallReadyAction + | ChatToolCallConfirmedAction + | ChatToolCallCompleteAction + | ChatToolCallResultConfirmedAction + | ChatToolCallContentChangedAction + | ChatTurnCompleteAction + | ChatTurnCancelledAction + | ChatErrorAction + | ChatUsageAction + | ChatReasoningAction + | ChatPendingMessageSetAction + | ChatPendingMessageRemovedAction + | ChatQueuedMessagesReorderedAction + | ChatInputRequestedAction + | ChatInputAnswerChangedAction + | ChatInputCompletedAction + | ChatTruncatedAction +; + +/** Union of chat actions that clients may dispatch. */ +export type ClientChatAction = + | ChatTurnStartedAction + | ChatToolCallConfirmedAction + | ChatToolCallCompleteAction + | ChatToolCallResultConfirmedAction + | ChatToolCallContentChangedAction + | ChatTurnCancelledAction + | ChatPendingMessageSetAction + | ChatPendingMessageRemovedAction + | ChatQueuedMessagesReorderedAction + | ChatInputAnswerChangedAction + | ChatInputCompletedAction + | ChatTruncatedAction +; + +/** Union of chat actions that only the server may produce. */ +export type ServerChatAction = + | ChatDeltaAction + | ChatResponsePartAction + | ChatToolCallStartAction + | ChatToolCallDeltaAction + | ChatToolCallReadyAction + | ChatTurnCompleteAction + | ChatErrorAction + | ChatUsageAction + | ChatReasoningAction + | ChatInputRequestedAction +; + /** Union of all terminal-scoped actions. */ export type TerminalAction = | TerminalDataAction @@ -301,45 +325,49 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.RootConfigChanged]: true, [ActionType.SessionReady]: false, [ActionType.SessionCreationFailed]: false, - [ActionType.SessionTurnStarted]: true, - [ActionType.SessionDelta]: false, - [ActionType.SessionResponsePart]: false, - [ActionType.SessionToolCallStart]: false, - [ActionType.SessionToolCallDelta]: false, - [ActionType.SessionToolCallReady]: false, - [ActionType.SessionToolCallConfirmed]: true, - [ActionType.SessionToolCallComplete]: true, - [ActionType.SessionToolCallResultConfirmed]: true, - [ActionType.SessionToolCallContentChanged]: true, - [ActionType.SessionTurnComplete]: false, - [ActionType.SessionTurnCancelled]: true, - [ActionType.SessionError]: false, + [ActionType.SessionChatAdded]: false, + [ActionType.SessionChatRemoved]: false, + [ActionType.SessionChatUpdated]: false, + [ActionType.SessionDefaultChatChanged]: false, [ActionType.SessionTitleChanged]: true, - [ActionType.SessionUsage]: false, - [ActionType.SessionReasoning]: false, [ActionType.SessionModelChanged]: true, [ActionType.SessionAgentChanged]: true, [ActionType.SessionServerToolsChanged]: false, [ActionType.SessionActiveClientChanged]: true, [ActionType.SessionActiveClientToolsChanged]: true, - [ActionType.SessionPendingMessageSet]: true, - [ActionType.SessionPendingMessageRemoved]: true, - [ActionType.SessionQueuedMessagesReordered]: true, - [ActionType.SessionInputRequested]: false, - [ActionType.SessionInputAnswerChanged]: true, - [ActionType.SessionInputCompleted]: true, [ActionType.SessionCustomizationsChanged]: false, [ActionType.SessionCustomizationToggled]: true, [ActionType.SessionCustomizationUpdated]: false, [ActionType.SessionCustomizationRemoved]: false, [ActionType.SessionMcpServerStateChanged]: false, - [ActionType.SessionTruncated]: true, [ActionType.SessionIsReadChanged]: true, [ActionType.SessionIsArchivedChanged]: true, [ActionType.SessionActivityChanged]: false, [ActionType.SessionChangesetsChanged]: false, [ActionType.SessionConfigChanged]: true, [ActionType.SessionMetaChanged]: false, + [ActionType.ChatTurnStarted]: true, + [ActionType.ChatDelta]: false, + [ActionType.ChatResponsePart]: false, + [ActionType.ChatToolCallStart]: false, + [ActionType.ChatToolCallDelta]: false, + [ActionType.ChatToolCallReady]: false, + [ActionType.ChatToolCallConfirmed]: true, + [ActionType.ChatToolCallComplete]: true, + [ActionType.ChatToolCallResultConfirmed]: true, + [ActionType.ChatToolCallContentChanged]: true, + [ActionType.ChatTurnComplete]: false, + [ActionType.ChatTurnCancelled]: true, + [ActionType.ChatError]: false, + [ActionType.ChatUsage]: false, + [ActionType.ChatReasoning]: false, + [ActionType.ChatPendingMessageSet]: true, + [ActionType.ChatPendingMessageRemoved]: true, + [ActionType.ChatQueuedMessagesReordered]: true, + [ActionType.ChatInputRequested]: false, + [ActionType.ChatInputAnswerChanged]: true, + [ActionType.ChatInputCompleted]: true, + [ActionType.ChatTruncated]: true, [ActionType.ChangesetStatusChanged]: false, [ActionType.ChangesetFileSet]: false, [ActionType.ChangesetFileRemoved]: false, diff --git a/types/actions.ts b/types/actions.ts index 66498160..a2469d66 100644 --- a/types/actions.ts +++ b/types/actions.ts @@ -10,6 +10,7 @@ export * from './common/actions.js'; export * from './channels-root/actions.js'; export * from './channels-session/actions.js'; +export * from './channels-chat/actions.js'; export * from './channels-terminal/actions.js'; export * from './channels-changeset/actions.js'; export * from './channels-annotations/actions.js'; diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts new file mode 100644 index 00000000..8ce604f8 --- /dev/null +++ b/types/channels-chat/actions.ts @@ -0,0 +1,544 @@ +/** + * Chat Channel Actions — Mutations of an `ahp-chat:` channel's state. + * + * @module channels-chat/actions + */ + +import { ActionType } from '../common/actions.js'; +import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo } from '../common/state.js'; +import type { + Message, + ResponsePart, + ToolCallResult, + ToolResultContent, + ChatInputAnswer, + ChatInputRequest, + ChatInputResponseKind, + ConfirmationOption, + ToolCallContributor, +} from './state.js'; +import { + ToolCallConfirmationReason, + ToolCallCancellationReason, + PendingMessageKind, +} from './state.js'; + +// ─── Tool Call Action Base ─────────────────────────────────────────────────── + +/** + * Base interface for all tool-call-scoped actions, carrying the common turn + * and tool call identifiers. The owning chat URI is identified by the + * enclosing {@link ActionEnvelope}'s `channel` field. + * + * @category Chat Actions + */ +interface ToolCallActionBase { + /** Turn identifier */ + turnId: string; + /** Tool call identifier */ + toolCallId: string; + /** + * Additional provider-specific metadata for this tool call. + * + * Clients MAY look for well-known keys here to provide enhanced UI. + * For example, a `ptyTerminal` key with `{ input: string; output: string }` + * indicates the tool operated on a terminal (both `input` and `output` may + * contain escape sequences). + */ + _meta?: Record; +} + + +// ─── Chat Actions ─────────────────────────────────────────────────────────── + +/** + * A new message has been sent to the agent, and a new turn starts. + * + * A client is only allowed to send {@link MessageKind.User} messages. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatTurnStartedAction { + type: ActionType.ChatTurnStarted; + /** Turn identifier */ + turnId: string; + /** The new message */ + message: Message; + /** If this turn was auto-started from a queued message, the ID of that message */ + queuedMessageId?: string; +} + +/** + * Streaming text chunk from the assistant, appended to a specific response part. + * + * The server MUST first emit a `chat/responsePart` to create the target + * part (markdown or reasoning), then use this action to append text to it. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatDeltaAction { + type: ActionType.ChatDelta; + /** Turn identifier */ + turnId: string; + /** Identifier of the response part to append to */ + partId: string; + /** Text chunk */ + content: string; +} + +/** + * Structured content appended to the response. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatResponsePartAction { + type: ActionType.ChatResponsePart; + /** Turn identifier */ + turnId: string; + /** Response part (markdown or content ref) */ + part: ResponsePart; +} + +/** + * A tool call begins — parameters are streaming from the LM. + * + * The server sets {@link ToolCallContributor | `contributor`} to identify + * the origin of the tool. For client-provided tools, the named client is + * responsible for executing the tool once it reaches the `running` state + * and dispatching `chat/toolCallComplete`. For MCP-served tools, the + * server executes the call against the named `McpServerCustomization`. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatToolCallStartAction extends ToolCallActionBase { + type: ActionType.ChatToolCallStart; + /** Internal tool name (for debugging/logging) */ + toolName: string; + /** Human-readable tool name */ + displayName: string; + /** + * Reference to the contributor of the tool being called. Absent for + * server-side tools that are not contributed by a client or MCP server. + */ + contributor?: ToolCallContributor; +} + +/** + * Streaming partial parameters for a tool call. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatToolCallDeltaAction extends ToolCallActionBase { + type: ActionType.ChatToolCallDelta; + /** Partial parameter content to append */ + content: string; + /** Updated progress message */ + invocationMessage?: StringOrMarkdown; +} + +/** + * Tool call parameters are complete, or a running tool requires re-confirmation. + * + * When dispatched for a `streaming` tool call, transitions to `pending-confirmation` + * or directly to `running` if `confirmed` is set. + * + * When dispatched for a `running` tool call (e.g. mid-execution permission needed), + * transitions back to `pending-confirmation`. The `invocationMessage` and `_meta` + * SHOULD be updated to describe the specific confirmation needed. Clients use the + * standard `chat/toolCallConfirmed` flow to approve or deny. + * + * For client-provided tools, the server typically sets `confirmed` to + * `'not-needed'` so the tool transitions directly to `running`, where the + * owning client can begin execution immediately. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatToolCallReadyAction extends ToolCallActionBase { + type: ActionType.ChatToolCallReady; + /** Message describing what the tool will do or what confirmation is needed */ + invocationMessage: StringOrMarkdown; + /** Raw tool input */ + toolInput?: string; + /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ + confirmationTitle?: StringOrMarkdown; + /** File edits that this tool call will perform, for preview before confirmation */ + edits?: { items: FileEdit[] }; + /** Whether the agent host allows the client to edit the tool's input parameters before confirming */ + editable?: boolean; + /** If set, the tool was auto-confirmed and transitions directly to `running` */ + confirmed?: ToolCallConfirmationReason; + /** + * Options the server offers for this confirmation. When present, the client + * SHOULD render these instead of a plain approve/deny UI. Each option + * belongs to a {@link ConfirmationOptionGroup} so the client can still + * categorise the choices. + */ + options?: ConfirmationOption[]; +} + +/** + * Client approves a pending tool call. The tool transitions to `running`. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatToolCallApprovedAction extends ToolCallActionBase { + type: ActionType.ChatToolCallConfirmed; + /** The tool call was approved */ + approved: true; + /** How the tool was confirmed */ + confirmed: ToolCallConfirmationReason; + /** Edited tool input parameters, if the client modified them before confirming */ + editedToolInput?: string; + /** ID of the selected confirmation option, if the server provided options */ + selectedOptionId?: string; +} + +/** + * Client denies a pending tool call. The tool transitions to `cancelled`. + * + * For client-provided tools, the owning client MUST dispatch this if it does + * not recognize the tool or cannot execute it. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatToolCallDeniedAction extends ToolCallActionBase { + type: ActionType.ChatToolCallConfirmed; + /** The tool call was denied */ + approved: false; + /** Why the tool was cancelled */ + reason: ToolCallCancellationReason.Denied | ToolCallCancellationReason.Skipped; + /** What the user suggested doing instead */ + userSuggestion?: Message; + /** Optional explanation for the denial */ + reasonMessage?: StringOrMarkdown; + /** ID of the selected confirmation option, if the server provided options */ + selectedOptionId?: string; +} + +/** + * Client confirms or denies a pending tool call. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export type ChatToolCallConfirmedAction = + | ChatToolCallApprovedAction + | ChatToolCallDeniedAction; + +/** + * Tool execution finished. Transitions to `completed` or `pending-result-confirmation` + * if `requiresResultConfirmation` is `true`. + * + * For client-provided tools (where `toolClientId` is set on the tool call state), + * the owning client dispatches this action with the execution result. The server + * SHOULD reject this action if the dispatching client does not match `toolClientId`. + * + * Servers waiting on a client tool call MAY time out after a reasonable duration + * if the implementing client disconnects or becomes unresponsive, and dispatch + * this action with `result.success = false` and an appropriate error. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatToolCallCompleteAction extends ToolCallActionBase { + type: ActionType.ChatToolCallComplete; + /** Execution result */ + result: ToolCallResult; + /** If true, the result requires client approval before finalizing */ + requiresResultConfirmation?: boolean; +} + +/** + * Client approves or denies a tool's result. + * + * If `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatToolCallResultConfirmedAction extends ToolCallActionBase { + type: ActionType.ChatToolCallResultConfirmed; + /** Whether the result was approved */ + approved: boolean; +} + +/** + * Partial content produced while a tool is still executing. + * + * Replaces the `content` array on the running tool call state. Clients can + * use this to display live feedback (e.g. a terminal reference) before the + * tool completes. + * + * For client-provided tools (where `toolClientId` is set on the tool call state), + * the owning client dispatches this action to stream intermediate content while + * executing. The server SHOULD reject this action if the dispatching client does + * not match `toolClientId`. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatToolCallContentChangedAction extends ToolCallActionBase { + type: ActionType.ChatToolCallContentChanged; + /** The current partial content for the running tool call */ + content: ToolResultContent[]; +} + +/** + * Turn finished — the assistant is idle. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatTurnCompleteAction { + type: ActionType.ChatTurnComplete; + /** Turn identifier */ + turnId: string; +} + +/** + * Turn was aborted; server stops processing. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatTurnCancelledAction { + type: ActionType.ChatTurnCancelled; + /** Turn identifier */ + turnId: string; +} + +/** + * Error during turn processing. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatErrorAction { + type: ActionType.ChatError; + /** Turn identifier */ + turnId: string; + /** Error details */ + error: ErrorInfo; +} + + +/** + * Token usage report for a turn. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatUsageAction { + type: ActionType.ChatUsage; + /** Turn identifier */ + turnId: string; + /** Token usage data */ + usage: UsageInfo; +} + +/** + * Reasoning/thinking text from the model, appended to a specific reasoning response part. + * + * The server MUST first emit a `chat/responsePart` to create the target + * reasoning part, then use this action to append text to it. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatReasoningAction { + type: ActionType.ChatReasoning; + /** Turn identifier */ + turnId: string; + /** Identifier of the reasoning response part to append to */ + partId: string; + /** Reasoning text chunk */ + content: string; +} + + +// ─── Truncation ────────────────────────────────────────────────────────────── + +/** + * Truncates a session's history. If `turnId` is provided, all turns after that + * turn are removed and the specified turn is kept. If `turnId` is omitted, all + * turns are removed. + * + * If there is an active turn it is silently dropped and the chat status + * returns to `idle`. + * + * Common use-case: truncate old data then dispatch a new + * `chat/turnStarted` with an edited message. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatTruncatedAction { + type: ActionType.ChatTruncated; + /** Keep turns up to and including this turn. Omit to clear all turns. */ + turnId?: string; +} + +// ─── Pending Message Actions ───────────────────────────────────────────────── + +/** + * A pending message was set (upsert semantics: creates or replaces). + * + * For steering messages, this always replaces the single steering message. + * For queued messages, if a message with the given `id` already exists it is + * updated in place; otherwise it is appended to the queue. If the chat is + * idle when a queued message is set, the server SHOULD immediately consume it + * and start a new turn. + * + * A client is only allowed to send {@link MessageKind.User} messages. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatPendingMessageSetAction { + type: ActionType.ChatPendingMessageSet; + /** Whether this is a steering or queued message */ + kind: PendingMessageKind; + /** Unique identifier for this pending message */ + id: string; + /** The message content */ + message: Message; +} + +/** + * A pending message was removed (steering or queued). + * + * Dispatched by clients to cancel a pending message, or by the server when + * it consumes a message (e.g. starting a turn from a queued message or + * injecting a steering message into the current turn). + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatPendingMessageRemovedAction { + type: ActionType.ChatPendingMessageRemoved; + /** Whether this is a steering or queued message */ + kind: PendingMessageKind; + /** Identifier of the pending message to remove */ + id: string; +} + +/** + * Reorder the queued messages. + * + * The `order` array contains the IDs of queued messages in their new + * desired order. IDs not present in the current queue are ignored. + * Queued messages whose IDs are absent from `order` are appended at + * the end in their original relative order (so a client with a stale + * view of the queue never silently drops messages). + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatQueuedMessagesReorderedAction { + type: ActionType.ChatQueuedMessagesReordered; + /** Queued message IDs in the desired order */ + order: string[]; +} + +// ─── Session Input Actions ────────────────────────────────────────────────── + +/** + * A session requested input from the user. + * + * Full-request upsert semantics: the `request` replaces any existing request + * with the same `id`, or is appended if it is new. Answer drafts are preserved + * unless `request.answers` is provided. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatInputRequestedAction { + type: ActionType.ChatInputRequested; + /** Input request to create or replace */ + request: ChatInputRequest; +} + +/** + * A client updated, submitted, skipped, or removed a single in-progress answer. + * + * Dispatching with `answer: undefined` removes that question's answer draft. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatInputAnswerChangedAction { + type: ActionType.ChatInputAnswerChanged; + /** Input request identifier */ + requestId: string; + /** Question identifier within the input request */ + questionId: string; + /** Updated answer, or `undefined` to clear an answer draft */ + answer?: ChatInputAnswer; +} + +/** + * A client accepted, declined, or cancelled a session input request. + * + * If accepted, the server uses `answers` (when provided) plus the request's + * synced answer state to resume the blocked operation. + * + * @category Chat Actions + * @version 1 + * @clientDispatchable + */ +export interface ChatInputCompletedAction { + type: ActionType.ChatInputCompleted; + /** Input request identifier */ + requestId: string; + /** Completion outcome */ + response: ChatInputResponseKind; + /** Optional final answer replacement, keyed by question ID */ + answers?: Record; +} + + +export type ChatAction = + | ChatTurnStartedAction + | ChatDeltaAction + | ChatResponsePartAction + | ChatToolCallStartAction + | ChatToolCallDeltaAction + | ChatToolCallReadyAction + | ChatToolCallConfirmedAction + | ChatToolCallCompleteAction + | ChatToolCallResultConfirmedAction + | ChatToolCallContentChangedAction + | ChatTurnCompleteAction + | ChatTurnCancelledAction + | ChatErrorAction + | ChatUsageAction + | ChatReasoningAction + | ChatTruncatedAction + | ChatPendingMessageSetAction + | ChatPendingMessageRemovedAction + | ChatQueuedMessagesReorderedAction + | ChatInputRequestedAction + | ChatInputAnswerChangedAction + | ChatInputCompletedAction +; diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts new file mode 100644 index 00000000..c44957f3 --- /dev/null +++ b/types/channels-chat/commands.ts @@ -0,0 +1,60 @@ +/** + * Chat Channel Commands — `createChat` and `disposeChat`. + * + * @module channels-chat/commands + */ + +import type { URI } from '../common/state.js'; +import type { BaseParams } from '../common/commands.js'; +import type { ModelSelection } from '../channels-root/state.js'; +import type { AgentSelection } from '../channels-session/state.js'; +import type { Message } from './state.js'; + +// ─── createChat ────────────────────────────────────────────────────────────── + +/** + * Identifies a source chat and turn to fork from. + */ +export interface ChatForkSource { + /** URI of the existing chat to fork from */ + chat: URI; + /** Turn ID in the source chat; content up to and including this turn's response is copied */ + turnId: string; +} + +/** + * Creates a new chat within a session. + * + * @category Commands + * @method createChat + * @direction Client → Server + * @messageType Request + * @version 1 + */ +export interface CreateChatParams extends BaseParams { + /** Session URI containing the new chat. */ + channel: URI; + /** Chat URI (client-chosen, e.g. `ahp-chat:/`). */ + chat: URI; + /** Optional initial message for the new chat. */ + initialMessage?: Message; + /** Optional per-chat model override. */ + model?: ModelSelection; + /** Optional per-chat agent override. */ + agent?: AgentSelection; + /** Optional source chat and turn to fork from. */ + source?: ChatForkSource; +} + +// ─── disposeChat ───────────────────────────────────────────────────────────── + +/** + * Disposes a chat and cleans up server-side resources. + * + * @category Commands + * @method disposeChat + * @direction Client → Server + * @messageType Request + * @version 1 + */ +export interface DisposeChatParams extends BaseParams {} diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts new file mode 100644 index 00000000..24d93841 --- /dev/null +++ b/types/channels-chat/reducer.ts @@ -0,0 +1,663 @@ +/** + * Chat Channel Reducer — Pure reducer for `ChatState`, including turn + * lifecycle, tool call transitions, pending messages, and input requests. + * + * @module channels-chat/reducer + */ + +import { ActionType } from '../common/actions.js'; +import type { + ChatInputRequest, + ChatState, + ToolCallState, + ResponsePart, + ToolCallResponsePart, + Turn, + PendingMessage, + ConfirmationOption, +} from './state.js'; +import { + TurnState, + ToolCallStatus, + ToolCallConfirmationReason, + ToolCallCancellationReason, + ResponsePartKind, + PendingMessageKind, +} from './state.js'; +import { SessionStatus } from '../channels-session/state.js'; +import type { ChatAction } from '../action-origin.generated.js'; +import { softAssertNever } from '../common/reducer-helpers.js'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** Extracts the common base fields shared by all tool call lifecycle states. */ +function tcBase(tc: ToolCallState) { + return { + toolCallId: tc.toolCallId, + toolName: tc.toolName, + displayName: tc.displayName, + contributor: tc.contributor, + _meta: tc._meta, + }; +} + +function tcBaseWithMeta(tc: ToolCallState, meta: Record | undefined) { + return { + ...tcBase(tc), + _meta: meta ?? tc._meta, + }; +} + +/** Resolves a selected option from the confirmation options array by ID. */ +function resolveSelectedOption(options: ConfirmationOption[] | undefined, id: string | undefined): ConfirmationOption | undefined { + if (!id || !options) { + return undefined; + } + return options.find(o => o.id === id); +} + +/** Returns `true` if the active turn has any tool call awaiting user confirmation. */ +function hasPendingToolCallConfirmation(state: ChatState): boolean { + if (!state.activeTurn) { + return false; + } + return state.activeTurn.responseParts.some(part => + part.kind === ResponsePartKind.ToolCall + && (part.toolCall.status === ToolCallStatus.PendingConfirmation + || part.toolCall.status === ToolCallStatus.PendingResultConfirmation), + ); +} + +/** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */ +const STATUS_ACTIVITY_MASK = (1 << 5) - 1; + +/** Sets or clears a metadata flag on a status value. */ +function withStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean): SessionStatus { + return set ? status | flag : status & ~flag; +} + +/** Derives the summary status from live session work, preserving orthogonal flags. */ +function summaryStatus(state: ChatState, terminalStatus?: SessionStatus.Error): SessionStatus { + let activity: SessionStatus; + if (terminalStatus) { + activity = terminalStatus; + } else if ((state.inputRequests?.length ?? 0) > 0 || hasPendingToolCallConfirmation(state)) { + activity = SessionStatus.InputNeeded; + } else if (state.activeTurn) { + activity = SessionStatus.InProgress; + } else { + activity = SessionStatus.Idle; + } + + return state.status & ~STATUS_ACTIVITY_MASK | activity; +} + +/** + * Returns a state with `status` recomputed. Use this after reducers + * that change data which feeds into {@link summaryStatus} (e.g. tool call + * lifecycle transitions that may enter or leave a pending-confirmation state). + */ +function refreshSummaryStatus(state: ChatState): ChatState { + const status = summaryStatus(state); + if (status === state.status) { + return state; + } + return { ...state, status }; +} + +/** + * Ends the active turn, finalizing it into a completed turn record. + * + * Tool call parts with non-terminal states are forced to cancelled. + * Pending permissions are stripped from tool call parts. + */ +function endTurn( + state: ChatState, + turnId: string, + turnState: TurnState, + terminalStatus?: SessionStatus.Error, + error?: { errorType: string; message: string; stack?: string }, +): ChatState { + if (!state.activeTurn || state.activeTurn.id !== turnId) { + return state; + } + const active = state.activeTurn; + + const responseParts: ResponsePart[] = active.responseParts.map(part => { + if (part.kind !== ResponsePartKind.ToolCall) { + return part; + } + const tc = part.toolCall; + if (tc.status === ToolCallStatus.Completed || tc.status === ToolCallStatus.Cancelled) { + return part; + } + // Force non-terminal tool calls into cancelled state + return { + kind: ResponsePartKind.ToolCall, + toolCall: { + status: ToolCallStatus.Cancelled as const, + ...tcBase(tc), + invocationMessage: tc.status === ToolCallStatus.Streaming ? (tc.invocationMessage ?? '') : tc.invocationMessage, + toolInput: tc.status === ToolCallStatus.Streaming ? undefined : tc.toolInput, + reason: ToolCallCancellationReason.Skipped, + }, + }; + }); + + const turn: Turn = { + id: active.id, + message: active.message, + responseParts, + usage: active.usage, + state: turnState, + error, + }; + + const next: ChatState = { + ...state, + turns: [...state.turns, turn], + activeTurn: undefined, + modifiedAt: new Date(Date.now()).toISOString(), + }; + delete next.inputRequests; + return { + ...next, + status: summaryStatus(next, terminalStatus), + }; +} + +function upsertInputRequest(state: ChatState, request: ChatInputRequest): ChatState { + const existing = state.inputRequests ?? []; + const idx = existing.findIndex(r => r.id === request.id); + const inputRequests = [...existing]; + if (idx >= 0) { + const answers = request.answers ?? inputRequests[idx].answers; + inputRequests[idx] = { ...request, answers }; + } else { + inputRequests.push(request); + } + const next = { ...state, inputRequests }; + return { ...next, status: withStatusFlag(summaryStatus(next), SessionStatus.IsRead, false), modifiedAt: new Date(Date.now()).toISOString() }; +} + +/** + * Immutably updates the tool call inside a `ToolCall` response part in the + * active turn's `responseParts` array. Returns `state` unchanged if the + * active turn or tool call doesn't match. + */ +function updateToolCallInParts( + state: ChatState, + turnId: string, + toolCallId: string, + updater: (tc: ToolCallState) => ToolCallState, +): ChatState { + const activeTurn = state.activeTurn; + if (!activeTurn || activeTurn.id !== turnId) { + return state; + } + + let found = false; + const responseParts = activeTurn.responseParts.map(part => { + if (part.kind === ResponsePartKind.ToolCall && part.toolCall.toolCallId === toolCallId) { + const updated = updater(part.toolCall); + if (updated === part.toolCall) { + return part; + } + found = true; + return { ...part, toolCall: updated }; + } + return part; + }); + + if (!found) { + return state; + } + + return { + ...state, + activeTurn: { ...activeTurn, responseParts }, + }; +} + +/** + * Immutably updates a response part by `partId` in the active turn. + * For markdown/reasoning parts, matches on `id`. For tool call parts, + * matches on `toolCall.toolCallId`. + */ +function updateResponsePart( + state: ChatState, + turnId: string, + partId: string, + updater: (part: ResponsePart) => ResponsePart, +): ChatState { + const activeTurn = state.activeTurn; + if (!activeTurn || activeTurn.id !== turnId) { + return state; + } + + let found = false; + const responseParts = activeTurn.responseParts.map(part => { + if (!found) { + const id = part.kind === ResponsePartKind.ToolCall + ? part.toolCall.toolCallId + : 'id' in part ? part.id : undefined; + if (id === partId) { + found = true; + return updater(part); + } + } + return part; + }); + + if (!found) { + return state; + } + + return { + ...state, + activeTurn: { ...activeTurn, responseParts }, + }; +} + + +// ─── Chat Reducer ──────────────────────────────────────────────────────────── + +/** + * Pure reducer for chat state. Handles all {@link ChatAction} variants. + */ +export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: string) => void): ChatState { + switch (action.type) { + // ── Turn Lifecycle ──────────────────────────────────────────────────── + + case ActionType.ChatTurnStarted: { + let next: ChatState = { + ...state, + activeTurn: { + id: action.turnId, + message: action.message, + responseParts: [], + usage: undefined, + }, + }; + next = { + ...next, + status: withStatusFlag(summaryStatus(next), SessionStatus.IsRead, false), + modifiedAt: new Date(Date.now()).toISOString(), + }; + + // If this turn was auto-started from a pending message, remove it + if (action.queuedMessageId) { + if (next.steeringMessage?.id === action.queuedMessageId) { + next = { ...next, steeringMessage: undefined }; + } + if (next.queuedMessages) { + const filtered = next.queuedMessages.filter(m => m.id !== action.queuedMessageId); + next = { ...next, queuedMessages: filtered.length > 0 ? filtered : undefined }; + } + } + + return next; + } + + case ActionType.ChatDelta: + return updateResponsePart(state, action.turnId, action.partId, part => { + if (part.kind === ResponsePartKind.Markdown) { + return { ...part, content: part.content + action.content }; + } + return part; + }); + + case ActionType.ChatResponsePart: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { + ...state.activeTurn, + responseParts: [...state.activeTurn.responseParts, action.part], + }, + }; + + case ActionType.ChatTurnComplete: + return endTurn(state, action.turnId, TurnState.Complete); + + case ActionType.ChatTurnCancelled: + return endTurn(state, action.turnId, TurnState.Cancelled); + + case ActionType.ChatError: + return endTurn(state, action.turnId, TurnState.Error, SessionStatus.Error, action.error); + + // ── Tool Call State Machine ─────────────────────────────────────────── + + case ActionType.ChatToolCallStart: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { + ...state.activeTurn, + responseParts: [ + ...state.activeTurn.responseParts, + { + kind: ResponsePartKind.ToolCall, + toolCall: { + toolCallId: action.toolCallId, + toolName: action.toolName, + displayName: action.displayName, + contributor: action.contributor, + _meta: action._meta, + status: ToolCallStatus.Streaming, + }, + } satisfies ToolCallResponsePart, + ], + }, + }; + + case ActionType.ChatToolCallDelta: + return updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.Streaming) { + return tc; + } + return { + ...tc, + ...(action._meta !== undefined ? { _meta: action._meta } : {}), + partialInput: (tc.partialInput ?? '') + action.content, + invocationMessage: action.invocationMessage ?? tc.invocationMessage, + }; + }); + + case ActionType.ChatToolCallReady: + return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.Streaming && tc.status !== ToolCallStatus.Running) { + return tc; + } + const base = tcBaseWithMeta(tc, action._meta); + if (action.confirmed) { + return { + status: ToolCallStatus.Running, + ...base, + invocationMessage: action.invocationMessage, + toolInput: action.toolInput, + confirmed: action.confirmed, + }; + } + return { + status: ToolCallStatus.PendingConfirmation, + ...base, + invocationMessage: action.invocationMessage, + toolInput: action.toolInput, + confirmationTitle: action.confirmationTitle, + edits: action.edits, + editable: action.editable, + ...(action.options ? { options: action.options } : {}), + }; + })); + + case ActionType.ChatToolCallConfirmed: + return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.PendingConfirmation) { + return tc; + } + const base = tcBaseWithMeta(tc, action._meta); + const selectedOption = resolveSelectedOption(tc.options, action.selectedOptionId); + if (action.approved) { + return { + status: ToolCallStatus.Running, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: action.editedToolInput ?? tc.toolInput, + confirmed: action.confirmed, + ...(selectedOption ? { selectedOption } : {}), + }; + } + return { + status: ToolCallStatus.Cancelled, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + reason: action.reason, + reasonMessage: action.reasonMessage, + userSuggestion: action.userSuggestion, + ...(selectedOption ? { selectedOption } : {}), + }; + })); + + case ActionType.ChatToolCallComplete: + return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.Running && tc.status !== ToolCallStatus.PendingConfirmation) { + return tc; + } + const base = tcBaseWithMeta(tc, action._meta); + const confirmed = tc.status === ToolCallStatus.Running + ? tc.confirmed + : ToolCallConfirmationReason.NotNeeded; + const selectedOption = tc.status === ToolCallStatus.Running + ? tc.selectedOption + : undefined; + if (action.requiresResultConfirmation) { + return { + status: ToolCallStatus.PendingResultConfirmation, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed, + ...(selectedOption ? { selectedOption } : {}), + ...action.result, + }; + } + return { + status: ToolCallStatus.Completed, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed, + ...(selectedOption ? { selectedOption } : {}), + ...action.result, + }; + })); + + case ActionType.ChatToolCallResultConfirmed: + return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.PendingResultConfirmation) { + return tc; + } + const base = tcBaseWithMeta(tc, action._meta); + if (action.approved) { + return { + status: ToolCallStatus.Completed, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed: tc.confirmed, + ...(tc.selectedOption ? { selectedOption: tc.selectedOption } : {}), + success: tc.success, + pastTenseMessage: tc.pastTenseMessage, + content: tc.content, + structuredContent: tc.structuredContent, + error: tc.error, + }; + } + return { + status: ToolCallStatus.Cancelled, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + reason: ToolCallCancellationReason.ResultDenied, + ...(tc.selectedOption ? { selectedOption: tc.selectedOption } : {}), + }; + })); + + case ActionType.ChatToolCallContentChanged: + return updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.Running) { + return tc; + } + return { + ...tc, + ...(action._meta !== undefined ? { _meta: action._meta } : {}), + content: action.content, + }; + }); + + + case ActionType.ChatUsage: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { ...state.activeTurn, usage: action.usage }, + }; + + case ActionType.ChatReasoning: + return updateResponsePart(state, action.turnId, action.partId, part => { + if (part.kind === ResponsePartKind.Reasoning) { + return { ...part, content: part.content + action.content }; + } + return part; + }); + + + // ── Truncation ──────────────────────────────────────────────────────── + + case ActionType.ChatTruncated: { + let turns: typeof state.turns; + if (action.turnId === undefined) { + turns = []; + } else { + const idx = state.turns.findIndex(t => t.id === action.turnId); + if (idx < 0) { + return state; + } + turns = state.turns.slice(0, idx + 1); + } + const next: ChatState = { + ...state, + turns, + activeTurn: undefined, + modifiedAt: new Date(Date.now()).toISOString(), + }; + delete next.inputRequests; + return { + ...next, + status: summaryStatus(next), + }; + } + + // ── Session Input Requests ───────────────────────────────────────────── + + case ActionType.ChatInputRequested: + return upsertInputRequest(state, action.request); + + case ActionType.ChatInputAnswerChanged: { + const existing = state.inputRequests; + const idx = existing?.findIndex(request => request.id === action.requestId) ?? -1; + if (!existing || idx < 0) { + return state; + } + const request = existing[idx]; + const answers = { ...(request.answers ?? {}) }; + if (action.answer === undefined) { + delete answers[action.questionId]; + } else { + answers[action.questionId] = action.answer; + } + const updated = [...existing]; + updated[idx] = { + ...request, + answers: Object.keys(answers).length > 0 ? answers : undefined, + }; + return { + ...state, + inputRequests: updated, + modifiedAt: new Date(Date.now()).toISOString(), + }; + } + + case ActionType.ChatInputCompleted: { + const existing = state.inputRequests; + if (!existing?.some(request => request.id === action.requestId)) { + return state; + } + const inputRequests = existing.filter(request => request.id !== action.requestId); + const next: ChatState = { + ...state, + }; + if (inputRequests.length > 0) { + next.inputRequests = inputRequests; + } else { + delete next.inputRequests; + } + return { + ...next, + status: summaryStatus(next), + modifiedAt: new Date(Date.now()).toISOString(), + }; + } + + // ── Pending Messages ────────────────────────────────────────────────── + + case ActionType.ChatPendingMessageSet: { + const entry: PendingMessage = { id: action.id, message: action.message }; + if (action.kind === PendingMessageKind.Steering) { + return { ...state, steeringMessage: entry }; + } + const existing = state.queuedMessages ?? []; + const idx = existing.findIndex(m => m.id === action.id); + if (idx >= 0) { + const updated = [...existing]; + updated[idx] = entry; + return { ...state, queuedMessages: updated }; + } + return { ...state, queuedMessages: [...existing, entry] }; + } + + case ActionType.ChatPendingMessageRemoved: { + if (action.kind === PendingMessageKind.Steering) { + if (!state.steeringMessage || state.steeringMessage.id !== action.id) { + return state; + } + return { ...state, steeringMessage: undefined }; + } + const existing = state.queuedMessages; + if (!existing) { + return state; + } + const filtered = existing.filter(m => m.id !== action.id); + return filtered.length === existing.length + ? state + : { ...state, queuedMessages: filtered.length > 0 ? filtered : undefined }; + } + + case ActionType.ChatQueuedMessagesReordered: { + const existing = state.queuedMessages; + if (!existing) { + return state; + } + const byId = new Map(existing.map(m => [m.id, m])); + const ordered = new Set(); + const reordered = action.order + .filter(id => { + if (byId.has(id) && !ordered.has(id)) { + ordered.add(id); + return true; + } + return false; + }) + .map(id => byId.get(id)!); + // Append any messages not mentioned in order, preserving original order + for (const m of existing) { + if (!ordered.has(m.id)) { + reordered.push(m); + } + } + return { ...state, queuedMessages: reordered }; + } + + default: + softAssertNever(action, log); + return state; + } +} diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts new file mode 100644 index 00000000..1b93dcd7 --- /dev/null +++ b/types/channels-chat/state.ts @@ -0,0 +1,1149 @@ +/** + * Chat State Types — Per-chat turns, messages, response parts, tool calls, + * and elicitation/input requests exposed on `ahp-chat:` channels. + * + * @module channels-chat/state + */ + +import type { ModelSelection } from '../channels-root/state.js'; +import type { AgentSelection, SessionStatus } from '../channels-session/state.js'; +import type { + ContentRef, + ErrorInfo, + FileEdit, + StringOrMarkdown, + TextRange, + TextSelection, + URI, + UsageInfo, +} from '../common/state.js'; + +// ─── Chat State ────────────────────────────────────────────────────────────── + +/** + * Full state for a single chat, loaded when a client subscribes to the chat's + * URI. + * + * The lightweight catalog representation of a chat is {@link ChatSummary}, + * carried in {@link SessionState.chats | `SessionState.chats`}. `ChatState` + * **denormalizes** every {@link ChatSummary} field directly onto itself so + * subscribers receive one flat object instead of having to merge a nested + * `summary` sub-object. Producers MUST keep the two representations + * consistent: any change to the inlined fields below SHOULD also be + * announced on the parent session via the matching + * {@link SessionChatUpdatedAction | `session/chatUpdated`} action. + * + * @category Chat State + */ +export interface ChatState { + // ── Summary fields (denormalized from ChatSummary) ───────────────── + /** Chat URI */ + resource: URI; + /** Chat title */ + title: string; + /** Current chat status (reuses SessionStatus shape) */ + status: SessionStatus; + /** Human-readable description of what the chat is currently doing */ + activity?: string; + /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ + modifiedAt: string; + /** Optional per-chat model override (defaults to the session's model) */ + model?: ModelSelection; + /** Optional per-chat agent override (defaults to the session's agent) */ + agent?: AgentSelection; + /** How this chat came into existence */ + origin?: ChatOrigin; + /** + * Optional per-chat working directory. + * + * If absent, the chat inherits + * {@link SessionSummary.workingDirectory | the session's working directory}. + * Hosts MAY override this for individual chats — for example, to give a + * subordinate chat its own git worktree so multiple chats in a session can + * make independent edits that the orchestrator later merges back. + */ + workingDirectory?: URI; + + // ── Conversation contents ────────────────────────────────────────── + /** Completed turns */ + turns: Turn[]; + /** Currently in-progress turn */ + activeTurn?: ActiveTurn; + /** Message to inject into the current turn at a convenient point */ + steeringMessage?: PendingMessage; + /** Messages to send automatically as new turns after the current turn finishes */ + queuedMessages?: PendingMessage[]; + /** Requests for user input that are currently blocking or informing chat progress */ + inputRequests?: ChatInputRequest[]; + /** + * Additional provider-specific metadata for this chat. + */ + _meta?: Record; +} + +/** + * Lightweight catalog entry for a chat, carried in + * {@link SessionState.chats | `SessionState.chats`}. The full conversation + * lives in {@link ChatState}, which inlines (denormalizes) every field below. + * + * @category Chat State + */ +export interface ChatSummary { + /** Chat URI */ + resource: URI; + /** Chat title */ + title: string; + /** Current chat status (reuses SessionStatus shape) */ + status: SessionStatus; + /** Human-readable description of what the chat is currently doing */ + activity?: string; + /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ + modifiedAt: string; + /** Optional per-chat model override (defaults to the session's model) */ + model?: ModelSelection; + /** Optional per-chat agent override (defaults to the session's agent) */ + agent?: AgentSelection; + /** How this chat came into existence */ + origin?: ChatOrigin; + /** + * Optional per-chat working directory. + * + * If absent, the chat inherits + * {@link SessionSummary.workingDirectory | the session's working directory}. + * See {@link ChatState.workingDirectory} for usage notes. + */ + workingDirectory?: URI; +} + +export const enum ChatOriginKind { + User = 'user', + Fork = 'fork', + Tool = 'tool', +} + +export type ChatOrigin = + | { kind: ChatOriginKind.User } + | { kind: ChatOriginKind.Fork; chat: URI; turnId: string } + | { kind: ChatOriginKind.Tool; chat: URI; toolCallId: string }; + +// ─── Pending Message Types ─────────────────────────────────────────────────── + +/** + * Discriminant for pending message kinds. + * + * @category Pending Message Types + */ +export const enum PendingMessageKind { + /** Injected into the current turn at a convenient point */ + Steering = 'steering', + /** Sent automatically as a new turn after the current turn finishes */ + Queued = 'queued', +} + +/** + * A message queued for future delivery to the agent. + * + * Steering messages are injected into the current turn mid-flight. + * Queued messages are automatically started as new turns after the + * current turn naturally finishes. + * + * @category Pending Message Types + */ +export interface PendingMessage { + /** Unique identifier for this pending message */ + id: string; + /** The message that will start the next turn */ + message: Message; +} + + +// ─── Chat Input Types ──────────────────────────────────────────────────── + +/** + * How a client completed an input request. + * + * @category Chat Input Types + */ +export const enum ChatInputResponseKind { + Accept = 'accept', + Decline = 'decline', + Cancel = 'cancel', +} + +/** + * Question/input control kind. + * + * @category Chat Input Types + */ +export const enum ChatInputQuestionKind { + Text = 'text', + Number = 'number', + Integer = 'integer', + Boolean = 'boolean', + SingleSelect = 'single-select', + MultiSelect = 'multi-select', +} + +/** + * A choice in a select-style question. + * + * @category Chat Input Types + */ +export interface ChatInputOption { + /** Stable option identifier; for MCP enum values this is the enum string */ + id: string; + /** Display label */ + label: string; + /** Optional secondary text */ + description?: string; + /** Whether this option is the recommended/default choice */ + recommended?: boolean; +} + +interface ChatInputQuestionBase { + /** Stable question identifier used as the key in `answers` */ + id: string; + /** Short display title */ + title?: string; + /** Prompt shown to the user */ + message: string; + /** Whether the user must answer this question to accept the request */ + required?: boolean; +} + +/** Text question within a chat input request. */ +export interface ChatInputTextQuestion extends ChatInputQuestionBase { + kind: ChatInputQuestionKind.Text; + /** Format hint for text questions, such as `email`, `uri`, `date`, or `date-time` */ + format?: string; + /** Minimum string length */ + min?: number; + /** Maximum string length */ + max?: number; + /** Default text */ + defaultValue?: string; +} + +/** Numeric question within a chat input request. */ +export interface ChatInputNumberQuestion extends ChatInputQuestionBase { + kind: ChatInputQuestionKind.Number | ChatInputQuestionKind.Integer; + /** + * Minimum value + * @format float + */ + min?: number; + /** + * Maximum value + * @format float + */ + max?: number; + /** + * Default numeric value + * @format float + */ + defaultValue?: number; +} + +/** Boolean question within a chat input request. */ +export interface ChatInputBooleanQuestion extends ChatInputQuestionBase { + kind: ChatInputQuestionKind.Boolean; + /** Default boolean value */ + defaultValue?: boolean; +} + +/** Single-select question within a chat input request. */ +export interface ChatInputSingleSelectQuestion extends ChatInputQuestionBase { + kind: ChatInputQuestionKind.SingleSelect; + /** Options the user may select from */ + options: ChatInputOption[]; + /** Whether the user may enter text instead of selecting an option */ + allowFreeformInput?: boolean; +} + +/** Multi-select question within a chat input request. */ +export interface ChatInputMultiSelectQuestion extends ChatInputQuestionBase { + kind: ChatInputQuestionKind.MultiSelect; + /** Options the user may select from */ + options: ChatInputOption[]; + /** Whether the user may enter text in addition to selecting options */ + allowFreeformInput?: boolean; + /** Minimum selected item count */ + min?: number; + /** Maximum selected item count */ + max?: number; +} + +/** + * One question within a chat input request. + * + * @category Chat Input Types + */ +export type ChatInputQuestion = ChatInputTextQuestion + | ChatInputNumberQuestion + | ChatInputBooleanQuestion + | ChatInputSingleSelectQuestion + | ChatInputMultiSelectQuestion; + +/** + * A live request for user input. + * + * The server creates or replaces requests with `chat/inputRequested`. + * Clients sync drafts with `chat/inputAnswerChanged` and complete requests + * with `chat/inputCompleted`. + * + * @category Chat Input Types + */ +export interface ChatInputRequest { + /** Stable request identifier */ + id: string; + /** Display message for the request as a whole */ + message?: string; + /** URL the user should review or open, for URL-style elicitations */ + url?: URI; + /** Ordered questions to ask the user */ + questions?: ChatInputQuestion[]; + /** Current draft or submitted answers, keyed by question ID */ + answers?: Record; +} + +/** + * Answer value kind. + * + * @category Chat Input Types + */ +export const enum ChatInputAnswerValueKind { + Text = 'text', + Number = 'number', + Boolean = 'boolean', + Selected = 'selected', + SelectedMany = 'selected-many', +} + +/** + * Value captured for one answer. + * + * @category Chat Input Types + */ +export interface ChatInputTextAnswerValue { + kind: ChatInputAnswerValueKind.Text; + value: string; +} + +export interface ChatInputNumberAnswerValue { + kind: ChatInputAnswerValueKind.Number; + /** @format float */ + value: number; +} + +export interface ChatInputBooleanAnswerValue { + kind: ChatInputAnswerValueKind.Boolean; + value: boolean; +} + +export interface ChatInputSelectedAnswerValue { + kind: ChatInputAnswerValueKind.Selected; + value: string; + /** Free-form text entered instead of selecting an option */ + freeformValues?: string[]; +} + +export interface ChatInputSelectedManyAnswerValue { + kind: ChatInputAnswerValueKind.SelectedMany; + value: string[]; + /** Free-form text entered in addition to selected options */ + freeformValues?: string[]; +} + +export type ChatInputAnswerValue = ChatInputTextAnswerValue + | ChatInputNumberAnswerValue + | ChatInputBooleanAnswerValue + | ChatInputSelectedAnswerValue + | ChatInputSelectedManyAnswerValue; + +export interface ChatInputAnswered { + /** Answer state */ + state: ChatInputAnswerState.Draft | ChatInputAnswerState.Submitted; + /** Answer value */ + value: ChatInputAnswerValue; +} + +export interface ChatInputSkipped { + /** Answer state */ + state: ChatInputAnswerState.Skipped; + /** Free-form reason or value captured while skipping, if any */ + freeformValues?: string[]; +} + +/** + * Answer lifecycle state. + * + * @category Chat Input Types + */ +export const enum ChatInputAnswerState { + Draft = 'draft', + Submitted = 'submitted', + Skipped = 'skipped', +} + +/** + * Draft, submitted, or skipped answer for one question. + * + * @category Chat Input Types + */ +export type ChatInputAnswer = ChatInputAnswered | ChatInputSkipped; + + +// ─── Turn Types ────────────────────────────────────────────────────────────── + +/** + * How a turn ended. + * + * @category Turn Types + */ +export const enum TurnState { + Complete = 'complete', + Cancelled = 'cancelled', + Error = 'error', +} + +/** + * Discriminant for {@link MessageAttachment} variants. + * + * @category Turn Types + */ +export const enum MessageAttachmentKind { + /** A simple, opaque attachment whose representation is described by the producer. */ + Simple = 'simple', + /** An attachment whose data is embedded inline as a base64 string. */ + EmbeddedResource = 'embeddedResource', + /** An attachment that references a resource by URI. */ + Resource = 'resource', + /** An attachment that references annotations on an annotations channel. */ + Annotations = 'annotations', +} + +/** + * A completed request/response cycle. + * + * @category Turn Types + */ +export interface Turn { + /** Turn identifier */ + id: string; + /** The message that initiated the turn */ + message: Message; + /** + * All response content in stream order: text, tool calls, reasoning, and content refs. + * + * Consumers should derive display text by concatenating markdown parts, + * and find tool calls by filtering for `ToolCall` parts. + */ + responseParts: ResponsePart[]; + /** Token usage info */ + usage: UsageInfo | undefined; + /** How the turn ended */ + state: TurnState; + /** Error details if state is `'error'` */ + error?: ErrorInfo; +} + +/** + * An in-progress turn — the assistant is actively streaming. + * + * @category Turn Types + */ +export interface ActiveTurn { + /** Turn identifier */ + id: string; + /** The message that initiated the turn */ + message: Message; + /** + * All response content in stream order: text, tool calls, reasoning, and content refs. + * + * Tool call parts include `pendingPermissions` when permissions are awaiting user approval. + */ + responseParts: ResponsePart[]; + /** Token usage info */ + usage: UsageInfo | undefined; +} + +/** + * Discriminant for Message types. + * + * @category Turn Types + */ +export enum MessageKind { + User = 'user', + SystemNotification = 'systemNotification', +} + +/** + * A message that initiates or steers a turn. Messages can originate from the + * user or be system-generated (see {@link MessageKind}). + * + * Attachments MAY be referenced inside {@link Message.text} via their + * {@link MessageAttachmentBase.range} field. Attachments without a range are + * still associated with the message but do not correspond to a specific span + * in the text. + * + * @category Turn Types + */ +export interface Message { + /** Message text */ + text: string; + /** The origin of the message */ + origin: { kind: MessageKind }; + /** File/selection attachments */ + attachments?: MessageAttachment[]; + /** + * Additional provider-specific metadata for this message. + * + * Clients MAY look for well-known keys here to provide enhanced UI, and + * agent hosts MAY use it to carry context that does not fit any other + * field. Mirrors the MCP `_meta` convention. + */ + _meta?: Record; +} + +/** + * Common fields shared by all {@link MessageAttachment} variants. + * + * @category Turn Types + */ +export interface MessageAttachmentBase { + /** + * A human-readable label for the attachment (e.g. the filename of a file + * attachment). Used for display in UI. + */ + label: string; + + /** + * If defined, the range in {@link Message.text} that references this + * attachment. This is a text range, not a byte range. + */ + range?: TextRange; + + /** + * Advisory display hint for clients rendering this attachment. Recognized + * values include: + * + * - `'image'`: the attachment is an image + * - `'document'`: the attachment is a textual document + * - `'symbol'`: the attachment is a code symbol (e.g. a function or class) + * - `'directory'`: the attachment is a folder + * - `'selection'`: the attachment is a selection within a document + * + * Implementations MAY provide additional values; clients SHOULD fall back + * to a reasonable default when an unknown value is encountered. + */ + displayKind?: string; + + /** + * Additional implementation-defined metadata for the attachment. + * + * If the attachment was produced by the `completions` command, the client + * MUST preserve every property of `_meta` originally returned by the agent + * host when sending the user message containing the accepted completion. + */ + _meta?: Record; +} + +/** + * A simple, opaque attachment whose model representation is described by + * the producer. + * + * @category Turn Types + */ +export interface SimpleMessageAttachment extends MessageAttachmentBase { + /** Discriminant */ + type: MessageAttachmentKind.Simple; + + /** + * Representation of the attachment as it should be shown to the model. + * + * If the attachment was produced by the client, this property MUST be + * defined so the agent host can correctly interpret the attachment. This + * property MAY be omitted when the attachment originated from a + * `completions` response. + */ + modelRepresentation?: string; +} + +/** + * An attachment whose data is embedded inline as a base64 string. + * + * Use this for small binary payloads (e.g. a pasted image) that should be + * delivered with the user message itself rather than fetched separately. + * + * @category Turn Types + */ +export interface MessageEmbeddedResourceAttachment extends MessageAttachmentBase { + /** Discriminant */ + type: MessageAttachmentKind.EmbeddedResource; + /** Base64-encoded binary data */ + data: string; + /** Content MIME type (e.g. `"image/png"`, `"application/pdf"`) */ + contentType: string; + /** + * Optional selection within the attached textual resource. + * + * Only meaningful for textual resources. + */ + selection?: TextSelection; +} + +/** + * An attachment that references a resource by URI. The content is not + * delivered inline; consumers can fetch it via `resourceRead` when needed. + * + * @category Turn Types + */ +export interface MessageResourceAttachment extends MessageAttachmentBase, ContentRef { + /** Discriminant */ + type: MessageAttachmentKind.Resource; + /** + * Optional selection within the referenced textual resource. + * + * Only meaningful for textual resources. + */ + selection?: TextSelection; +} + +/** + * An attachment that references annotations on a session's annotations + * channel (see {@link AnnotationsState}). + * + * When {@link annotationIds} is omitted the attachment references every + * annotation on the channel; when present it references only the listed + * {@link Annotation.id | annotation ids}. + * + * @category Turn Types + */ +export interface MessageAnnotationsAttachment extends MessageAttachmentBase { + /** Discriminant */ + type: MessageAttachmentKind.Annotations; + /** + * The annotations channel URI (typically `ahp-session://annotations`). + * Matches {@link AnnotationsSummary.resource}. + */ + resource: URI; + /** + * Specific {@link Annotation.id | annotation ids} to reference. When + * omitted, the attachment references all annotations on the channel. + */ + annotationIds?: string[]; +} + +/** + * An attachment associated with a {@link Message}. + * + * @category Turn Types + */ +export type MessageAttachment = + | SimpleMessageAttachment + | MessageEmbeddedResourceAttachment + | MessageResourceAttachment + | MessageAnnotationsAttachment; + +// ─── Response Parts ────────────────────────────────────────────────────────── + +/** + * Discriminant for response part types. + * + * @category Response Parts + */ +export const enum ResponsePartKind { + Markdown = 'markdown', + ContentRef = 'contentRef', + ToolCall = 'toolCall', + Reasoning = 'reasoning', + SystemNotification = 'systemNotification', +} + +/** + * @category Response Parts + */ +export interface MarkdownResponsePart { + /** Discriminant */ + kind: ResponsePartKind.Markdown; + /** Part identifier, used by `chat/delta` to target this part for content appends */ + id: string; + /** Markdown content */ + content: string; +} + +/** + * A content part that's a reference to large content stored outside the state tree. + * + * @category Response Parts + */ +export interface ResourceReponsePart extends ContentRef { + /** Discriminant */ + kind: ResponsePartKind.ContentRef; +} + +/** + * A tool call represented as a response part. + * + * Tool calls are part of the response stream, interleaved with text and + * reasoning. The `toolCall.toolCallId` serves as the part identifier for + * actions that target this part. + * + * @category Response Parts + */ +export interface ToolCallResponsePart { + /** Discriminant */ + kind: ResponsePartKind.ToolCall; + /** Full tool call lifecycle state */ + toolCall: ToolCallState; +} + +/** + * Reasoning/thinking content from the model. + * + * @category Response Parts + */ +export interface ReasoningResponsePart { + /** Discriminant */ + kind: ResponsePartKind.Reasoning; + /** Part identifier, used by `chat/reasoning` to target this part for content appends */ + id: string; + /** Accumulated reasoning text */ + content: string; +} + +/** + * @category Response Parts + */ +export type ResponsePart = + | MarkdownResponsePart + | ResourceReponsePart + | ToolCallResponsePart + | ReasoningResponsePart + | SystemNotificationResponsePart; + +/** + * A system notification surfaced as part of the response stream. + * + * System notifications are messages authored by the agent harness + * that need to be visible to both the agent (for situational awareness) and + * the user (for transcript continuity). Examples include "background subagent + * X completed" or "task Y was cancelled". + * + * @category Response Parts + */ +export interface SystemNotificationResponsePart { + /** Discriminant */ + kind: ResponsePartKind.SystemNotification; + /** The text of the system notification */ + content: StringOrMarkdown; +} + + +// ─── Tool Call Types ───────────────────────────────────────────────────────── + +/** + * Status of a tool call in the lifecycle state machine. + * + * @category Tool Call Types + */ +export const enum ToolCallStatus { + Streaming = 'streaming', + PendingConfirmation = 'pending-confirmation', + Running = 'running', + PendingResultConfirmation = 'pending-result-confirmation', + Completed = 'completed', + Cancelled = 'cancelled', +} + +/** + * How a tool call was confirmed for execution. + * + * - `NotNeeded` — No confirmation required (auto-approved) + * - `UserAction` — User explicitly approved + * - `Setting` — Approved by a persistent user setting + * + * @category Tool Call Types + */ +export const enum ToolCallConfirmationReason { + NotNeeded = 'not-needed', + UserAction = 'user-action', + Setting = 'setting', +} + +/** + * Why a tool call was cancelled. + * + * @category Tool Call Types + */ +export const enum ToolCallCancellationReason { + Denied = 'denied', + Skipped = 'skipped', + ResultDenied = 'result-denied', +} + +/** + * Whether a confirmation option represents an approval or denial action. + * + * @category Tool Call Types + */ +export const enum ConfirmationOptionKind { + Approve = 'approve', + Deny = 'deny', +} + +/** + * A confirmation option that the server offers for a tool call awaiting + * approval. Allows richer choices beyond simple approve/deny — for example, + * "Approve in this Session" or "Deny with reason." + * + * @category Tool Call Types + */ +export interface ConfirmationOption { + /** Unique identifier for the option, returned in the confirmed action */ + id: string; + /** Human-readable label displayed to the user */ + label: string; + /** Whether this option represents an approval or denial */ + kind: ConfirmationOptionKind; + /** + * Logical group number for visual categorisation. + * + * Clients SHOULD display options in the order they are defined and MAY + * use differing group numbers to insert dividers between logical clusters + * of options. + */ + group?: number; +} + +export const enum ToolCallContributorKind { + Client = 'client', + MCP = 'mcp', +} + +export interface ToolCallClientContributor { + kind: ToolCallContributorKind.Client; + /** + * If this tool is provided by a client, the `clientId` of the owning client. + * Absent for server-side tools. + * + * When set, the identified client is responsible for executing the tool and + * dispatching `chat/toolCallComplete` with the result. + */ + clientId: string; +} + +export interface ToolCallMcpContributor { + kind: ToolCallContributorKind.MCP; + /** + * Customization ID of the corresponding MCP server in {@link SessionState.customizations}. + */ + customizationId: string; +} + +export type ToolCallContributor = ToolCallClientContributor | ToolCallMcpContributor; + +/** + * Metadata common to all tool call states. + * + * @category Tool Call Types + * @remarks + * Fields like `toolName` carry agent-specific identifiers on the wire despite the + * agent-agnostic design principle. These exist for debugging and logging purposes. + * A future version may move these to a separate diagnostic channel or namespace them + * more clearly. + */ +interface ToolCallBase { + /** Unique tool call identifier */ + toolCallId: string; + /** Internal tool name (for debugging/logging) */ + toolName: string; + /** Human-readable tool name */ + displayName: string; + /** + * Reference to the contributor of the tool being called. + */ + contributor?: ToolCallContributor; + /** + * Additional provider-specific metadata for this tool call. + * + * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) + * `McpUiToolMeta` found in MCP tool calls, which may be used in combination + * with the {@link contributor} to serve MCP Apps. + */ + _meta?: Record; +} + +/** + * Properties available once tool call parameters are fully received. + * + * @category Tool Call Types + */ +interface ToolCallParameterFields { + /** Message describing what the tool will do */ + invocationMessage: StringOrMarkdown; + /** Raw tool input */ + toolInput?: string; +} + +/** + * Tool execution result details, available after execution completes. + * + * @category Tool Call Types + */ +export interface ToolCallResult { + /** Whether the tool succeeded */ + success: boolean; + /** Past-tense description of what the tool did */ + pastTenseMessage: StringOrMarkdown; + /** + * Unstructured result content blocks. + * + * This mirrors the `content` field of MCP `CallToolResult`. + */ + content?: ToolResultContent[]; + /** + * Optional structured result object. + * + * This mirrors the `structuredContent` field of MCP `CallToolResult`. + */ + structuredContent?: Record; + /** Error details if the tool failed */ + error?: { message: string; code?: string }; +} + +/** + * LM is streaming the tool call parameters. + * + * @category Tool Call Types + */ +export interface ToolCallStreamingState extends ToolCallBase { + status: ToolCallStatus.Streaming; + /** Partial parameters accumulated so far */ + partialInput?: string; + /** Progress message shown while parameters are streaming */ + invocationMessage?: StringOrMarkdown; +} + +/** + * Parameters are complete, or a running tool requires re-confirmation + * (e.g. a mid-execution permission check). + * + * @category Tool Call Types + */ +export interface ToolCallPendingConfirmationState extends ToolCallBase, ToolCallParameterFields { + status: ToolCallStatus.PendingConfirmation; + /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ + confirmationTitle?: StringOrMarkdown; + /** File edits that this tool call will perform, for preview before confirmation */ + edits?: { items: FileEdit[] }; + /** Whether the agent host allows the client to edit the tool's input parameters before confirming */ + editable?: boolean; + /** + * Options the server offers for this confirmation. When present, the client + * SHOULD render these instead of a plain approve/deny UI. Each option + * belongs to a {@link ConfirmationOptionGroup} so the client can still + * categorise the choices. + */ + options?: ConfirmationOption[]; +} + +/** + * Tool is actively executing. + * + * @category Tool Call Types + */ +export interface ToolCallRunningState extends ToolCallBase, ToolCallParameterFields { + status: ToolCallStatus.Running; + /** How the tool was confirmed for execution */ + confirmed: ToolCallConfirmationReason; + /** The confirmation option the user selected, if confirmation options were provided */ + selectedOption?: ConfirmationOption; + /** + * Partial content produced while the tool is still executing. + * + * For example, a terminal content block lets clients subscribe to live + * output before the tool completes. + */ + content?: ToolResultContent[]; +} + +/** + * Tool finished executing, waiting for client to approve the result. + * + * @category Tool Call Types + */ +export interface ToolCallPendingResultConfirmationState extends ToolCallBase, ToolCallParameterFields, ToolCallResult { + status: ToolCallStatus.PendingResultConfirmation; + /** How the tool was confirmed for execution */ + confirmed: ToolCallConfirmationReason; + /** The confirmation option the user selected, if confirmation options were provided */ + selectedOption?: ConfirmationOption; +} + +/** + * Tool completed successfully or with an error. + * + * @category Tool Call Types + */ +export interface ToolCallCompletedState extends ToolCallBase, ToolCallParameterFields, ToolCallResult { + status: ToolCallStatus.Completed; + /** How the tool was confirmed for execution */ + confirmed: ToolCallConfirmationReason; + /** The confirmation option the user selected, if confirmation options were provided */ + selectedOption?: ConfirmationOption; +} + +/** + * Tool call was cancelled before execution. + * + * @category Tool Call Types + */ +export interface ToolCallCancelledState extends ToolCallBase, ToolCallParameterFields { + status: ToolCallStatus.Cancelled; + /** Why the tool was cancelled */ + reason: ToolCallCancellationReason; + /** Optional message explaining the cancellation */ + reasonMessage?: StringOrMarkdown; + /** What the user suggested doing instead */ + userSuggestion?: Message; + /** The confirmation option the user selected, if confirmation options were provided */ + selectedOption?: ConfirmationOption; +} + +/** + * Discriminated union of all tool call lifecycle states. + * + * See the [state model guide](/guide/state-model.html#tool-call-lifecycle) + * for the full state machine diagram. + * + * @category Tool Call Types + */ +export type ToolCallState = + | ToolCallStreamingState + | ToolCallPendingConfirmationState + | ToolCallRunningState + | ToolCallPendingResultConfirmationState + | ToolCallCompletedState + | ToolCallCancelledState; + + +// ─── Tool Result Content ───────────────────────────────────────────────────── + +/** + * Discriminant for tool result content types. + * + * @category Tool Result Content + */ +export const enum ToolResultContentType { + Text = 'text', + EmbeddedResource = 'embeddedResource', + Resource = 'resource', + FileEdit = 'fileEdit', + Terminal = 'terminal', + Subagent = 'subagent', +} + +/** + * Text content in a tool result. + * + * Mirrors MCP `TextContent`. + * + * @category Tool Result Content + */ +export interface ToolResultTextContent { + type: ToolResultContentType.Text; + /** The text content */ + text: string; +} + +/** + * Base64-encoded binary content embedded in a tool result. + * + * Mirrors MCP `EmbeddedResource` for inline binary data. + * + * @category Tool Result Content + */ +export interface ToolResultEmbeddedResourceContent { + type: ToolResultContentType.EmbeddedResource; + /** Base64-encoded data */ + data: string; + /** Content type (e.g. `"image/png"`, `"application/pdf"`) */ + contentType: string; +} + +/** + * A reference to a resource stored outside the tool result. + * + * Wraps {@link ContentRef} for lazy-loading large results. + * + * @category Tool Result Content + */ +export interface ToolResultResourceContent extends ContentRef { + type: ToolResultContentType.Resource; +} + +/** + * Describes a file modification performed by a tool. + * + * @category Tool Result Content + */ +export interface ToolResultFileEditContent extends FileEdit { + type: ToolResultContentType.FileEdit; +} + +/** + * A reference to a terminal whose output is relevant to this tool result. + * + * Clients can subscribe to the terminal's URI to stream its output in real + * time, providing live feedback while a tool is executing. + * + * @category Tool Result Content + */ +export interface ToolResultTerminalContent { + type: ToolResultContentType.Terminal; + /** Terminal URI (subscribable for full terminal state) */ + resource: URI; + /** Display title for the terminal content */ + title: string; +} + +/** + * A reference to a subagent session spawned by a tool. + * + * Clients can subscribe to the subagent's session URI to stream its + * progress in real time, including inner tool calls and responses. + * + * @category Tool Result Content + */ +export interface ToolResultSubagentContent { + type: ToolResultContentType.Subagent; + /** Subagent session URI (subscribable for full session state) */ + resource: URI; + /** Display title for the subagent */ + title: string; + /** Internal agent name */ + agentName?: string; + /** Human-readable description of the subagent's task */ + description?: string; +} + +/** + * Content block in a tool result. + * + * Mirrors the content blocks in MCP `CallToolResult.content`, plus + * `ToolResultResourceContent` for lazy-loading large results, + * `ToolResultFileEditContent` for file edit diffs, + * `ToolResultTerminalContent` for live terminal output, and + * `ToolResultSubagentContent` for subagent sessions (AHP extensions). + * + * @category Tool Result Content + */ +export type ToolResultContent = + | ToolResultTextContent + | ToolResultEmbeddedResourceContent + | ToolResultResourceContent + | ToolResultFileEditContent + | ToolResultTerminalContent + | ToolResultSubagentContent; + diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 1cc68d84..acdfa979 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -5,56 +5,18 @@ */ import { ActionType } from '../common/actions.js'; -import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo } from '../common/state.js'; +import type { ErrorInfo } from '../common/state.js'; import type { - Message, - ResponsePart, - ToolCallResult, - ToolResultContent, ToolDefinition, SessionActiveClient, Customization, McpServerState, - SessionInputAnswer, - SessionInputRequest, - SessionInputResponseKind, - ConfirmationOption, AgentSelection, - ToolCallContributor, } from './state.js'; import type { ModelSelection } from '../channels-root/state.js'; import type { URI } from '../common/state.js'; -import { - ToolCallConfirmationReason, - ToolCallCancellationReason, - PendingMessageKind, -} from './state.js'; import type { Changeset } from '../channels-changeset/state.js'; - -// ─── Tool Call Action Base ─────────────────────────────────────────────────── - -/** - * Base interface for all tool-call-scoped actions, carrying the common turn - * and tool call identifiers. The owning session URI is identified by the - * enclosing {@link ActionEnvelope}'s `channel` field. - * - * @category Session Actions - */ -interface ToolCallActionBase { - /** Turn identifier */ - turnId: string; - /** Tool call identifier */ - toolCallId: string; - /** - * Additional provider-specific metadata for this tool call. - * - * Clients MAY look for well-known keys here to provide enhanced UI. - * For example, a `ptyTerminal` key with `{ input: string; output: string }` - * indicates the tool operated on a terminal (both `input` and `output` may - * contain escape sequences). - */ - _meta?: Record; -} +import type { ChatSummary } from '../channels-chat/state.js'; // ─── Session Actions ───────────────────────────────────────────────────────── @@ -81,289 +43,71 @@ export interface SessionCreationFailedAction { } /** - * A new message has been sent to the agent, and a new turn starts. - * - * A client is only allowed to send {@link MessageKind.User} messages. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionTurnStartedAction { - type: ActionType.SessionTurnStarted; - /** Turn identifier */ - turnId: string; - /** The new message */ - message: Message; - /** If this turn was auto-started from a queued message, the ID of that message */ - queuedMessageId?: string; -} - -/** - * Streaming text chunk from the assistant, appended to a specific response part. - * - * The server MUST first emit a `session/responsePart` to create the target - * part (markdown or reasoning), then use this action to append text to it. + * A chat was added to this session's catalog. Upsert semantics: if a chat + * with the same `summary.resource` already exists, the existing entry is + * replaced. * - * @category Session Actions - * @version 1 - */ -export interface SessionDeltaAction { - type: ActionType.SessionDelta; - /** Turn identifier */ - turnId: string; - /** Identifier of the response part to append to */ - partId: string; - /** Text chunk */ - content: string; -} - -/** - * Structured content appended to the response. + * Mirrors the root-channel `root/sessionAdded` notification. * * @category Session Actions * @version 1 */ -export interface SessionResponsePartAction { - type: ActionType.SessionResponsePart; - /** Turn identifier */ - turnId: string; - /** Response part (markdown or content ref) */ - part: ResponsePart; +export interface SessionChatAddedAction { + type: ActionType.SessionChatAdded; + /** The full summary of the newly added (or upserted) chat. */ + summary: ChatSummary; } /** - * A tool call begins — parameters are streaming from the LM. + * A chat was removed from this session's catalog. No-op when no entry matches. * - * The server sets {@link ToolCallContributor | `contributor`} to identify - * the origin of the tool. For client-provided tools, the named client is - * responsible for executing the tool once it reaches the `running` state - * and dispatching `session/toolCallComplete`. For MCP-served tools, the - * server executes the call against the named `McpServerCustomization`. - * - * @category Session Actions - * @version 1 - */ -export interface SessionToolCallStartAction extends ToolCallActionBase { - type: ActionType.SessionToolCallStart; - /** Internal tool name (for debugging/logging) */ - toolName: string; - /** Human-readable tool name */ - displayName: string; - /** - * Reference to the contributor of the tool being called. Absent for - * server-side tools that are not contributed by a client or MCP server. - */ - contributor?: ToolCallContributor; -} - -/** - * Streaming partial parameters for a tool call. + * Mirrors the root-channel `root/sessionRemoved` notification. * * @category Session Actions * @version 1 */ -export interface SessionToolCallDeltaAction extends ToolCallActionBase { - type: ActionType.SessionToolCallDelta; - /** Partial parameter content to append */ - content: string; - /** Updated progress message */ - invocationMessage?: StringOrMarkdown; +export interface SessionChatRemovedAction { + type: ActionType.SessionChatRemoved; + /** The URI of the chat to remove. */ + chat: URI; } /** - * Tool call parameters are complete, or a running tool requires re-confirmation. + * One existing chat's summary fields changed. * - * When dispatched for a `streaming` tool call, transitions to `pending-confirmation` - * or directly to `running` if `confirmed` is set. + * Partial-update semantics: only fields present in `changes` are written; + * omitted fields are preserved. Identity fields (`resource`) MUST NOT be + * carried in `changes`. No-op when no entry with `chat` exists — clients + * SHOULD then wait for a {@link SessionChatAddedAction | `session/chatAdded`}. * - * When dispatched for a `running` tool call (e.g. mid-execution permission needed), - * transitions back to `pending-confirmation`. The `invocationMessage` and `_meta` - * SHOULD be updated to describe the specific confirmation needed. Clients use the - * standard `session/toolCallConfirmed` flow to approve or deny. - * - * For client-provided tools, the server typically sets `confirmed` to - * `'not-needed'` so the tool transitions directly to `running`, where the - * owning client can begin execution immediately. + * Mirrors the root-channel `root/sessionSummaryChanged` notification. * * @category Session Actions * @version 1 */ -export interface SessionToolCallReadyAction extends ToolCallActionBase { - type: ActionType.SessionToolCallReady; - /** Message describing what the tool will do or what confirmation is needed */ - invocationMessage: StringOrMarkdown; - /** Raw tool input */ - toolInput?: string; - /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ - confirmationTitle?: StringOrMarkdown; - /** File edits that this tool call will perform, for preview before confirmation */ - edits?: { items: FileEdit[] }; - /** Whether the agent host allows the client to edit the tool's input parameters before confirming */ - editable?: boolean; - /** If set, the tool was auto-confirmed and transitions directly to `running` */ - confirmed?: ToolCallConfirmationReason; +export interface SessionChatUpdatedAction { + type: ActionType.SessionChatUpdated; + /** The URI of the chat whose summary changed. */ + chat: URI; /** - * Options the server offers for this confirmation. When present, the client - * SHOULD render these instead of a plain approve/deny UI. Each option - * belongs to a {@link ConfirmationOptionGroup} so the client can still - * categorise the choices. + * Mutable summary fields that changed; omitted fields are unchanged. + * + * Identity fields (`resource`) never change and MUST be omitted by + * senders; receivers SHOULD ignore them if present. */ - options?: ConfirmationOption[]; -} - -/** - * Client approves a pending tool call. The tool transitions to `running`. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionToolCallApprovedAction extends ToolCallActionBase { - type: ActionType.SessionToolCallConfirmed; - /** The tool call was approved */ - approved: true; - /** How the tool was confirmed */ - confirmed: ToolCallConfirmationReason; - /** Edited tool input parameters, if the client modified them before confirming */ - editedToolInput?: string; - /** ID of the selected confirmation option, if the server provided options */ - selectedOptionId?: string; -} - -/** - * Client denies a pending tool call. The tool transitions to `cancelled`. - * - * For client-provided tools, the owning client MUST dispatch this if it does - * not recognize the tool or cannot execute it. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionToolCallDeniedAction extends ToolCallActionBase { - type: ActionType.SessionToolCallConfirmed; - /** The tool call was denied */ - approved: false; - /** Why the tool was cancelled */ - reason: ToolCallCancellationReason.Denied | ToolCallCancellationReason.Skipped; - /** What the user suggested doing instead */ - userSuggestion?: Message; - /** Optional explanation for the denial */ - reasonMessage?: StringOrMarkdown; - /** ID of the selected confirmation option, if the server provided options */ - selectedOptionId?: string; -} - -/** - * Client confirms or denies a pending tool call. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export type SessionToolCallConfirmedAction = - | SessionToolCallApprovedAction - | SessionToolCallDeniedAction; - -/** - * Tool execution finished. Transitions to `completed` or `pending-result-confirmation` - * if `requiresResultConfirmation` is `true`. - * - * For client-provided tools (where `toolClientId` is set on the tool call state), - * the owning client dispatches this action with the execution result. The server - * SHOULD reject this action if the dispatching client does not match `toolClientId`. - * - * Servers waiting on a client tool call MAY time out after a reasonable duration - * if the implementing client disconnects or becomes unresponsive, and dispatch - * this action with `result.success = false` and an appropriate error. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionToolCallCompleteAction extends ToolCallActionBase { - type: ActionType.SessionToolCallComplete; - /** Execution result */ - result: ToolCallResult; - /** If true, the result requires client approval before finalizing */ - requiresResultConfirmation?: boolean; -} - -/** - * Client approves or denies a tool's result. - * - * If `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionToolCallResultConfirmedAction extends ToolCallActionBase { - type: ActionType.SessionToolCallResultConfirmed; - /** Whether the result was approved */ - approved: boolean; -} - -/** - * Partial content produced while a tool is still executing. - * - * Replaces the `content` array on the running tool call state. Clients can - * use this to display live feedback (e.g. a terminal reference) before the - * tool completes. - * - * For client-provided tools (where `toolClientId` is set on the tool call state), - * the owning client dispatches this action to stream intermediate content while - * executing. The server SHOULD reject this action if the dispatching client does - * not match `toolClientId`. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionToolCallContentChangedAction extends ToolCallActionBase { - type: ActionType.SessionToolCallContentChanged; - /** The current partial content for the running tool call */ - content: ToolResultContent[]; -} - -/** - * Turn finished — the assistant is idle. - * - * @category Session Actions - * @version 1 - */ -export interface SessionTurnCompleteAction { - type: ActionType.SessionTurnComplete; - /** Turn identifier */ - turnId: string; -} - -/** - * Turn was aborted; server stops processing. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionTurnCancelledAction { - type: ActionType.SessionTurnCancelled; - /** Turn identifier */ - turnId: string; + changes: Partial; } /** - * Error during turn processing. + * The default chat input-routing hint for this session changed. * * @category Session Actions * @version 1 */ -export interface SessionErrorAction { - type: ActionType.SessionError; - /** Turn identifier */ - turnId: string; - /** Error details */ - error: ErrorInfo; +export interface SessionDefaultChatChangedAction { + type: ActionType.SessionDefaultChatChanged; + /** New default chat URI, or `undefined` to clear the hint. */ + defaultChat?: URI; } /** @@ -380,39 +124,6 @@ export interface SessionTitleChangedAction { title: string; } -/** - * Token usage report for a turn. - * - * @category Session Actions - * @version 1 - */ -export interface SessionUsageAction { - type: ActionType.SessionUsage; - /** Turn identifier */ - turnId: string; - /** Token usage data */ - usage: UsageInfo; -} - -/** - * Reasoning/thinking text from the model, appended to a specific reasoning response part. - * - * The server MUST first emit a `session/responsePart` to create the target - * reasoning part, then use this action to append text to it. - * - * @category Session Actions - * @version 1 - */ -export interface SessionReasoningAction { - type: ActionType.SessionReasoning; - /** Turn identifier */ - turnId: string; - /** Identifier of the reasoning response part to append to */ - partId: string; - /** Reasoning text chunk */ - content: string; -} - /** * Model changed for this session. * @@ -711,148 +422,3 @@ export interface SessionMetaChangedAction { /** New `_meta` payload, or `undefined` to clear it */ _meta: Record | undefined; } - -// ─── Truncation ────────────────────────────────────────────────────────────── - -/** - * Truncates a session's history. If `turnId` is provided, all turns after that - * turn are removed and the specified turn is kept. If `turnId` is omitted, all - * turns are removed. - * - * If there is an active turn it is silently dropped and the session status - * returns to `idle`. - * - * Common use-case: truncate old data then dispatch a new - * `session/turnStarted` with an edited message. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionTruncatedAction { - type: ActionType.SessionTruncated; - /** Keep turns up to and including this turn. Omit to clear all turns. */ - turnId?: string; -} - -// ─── Pending Message Actions ───────────────────────────────────────────────── - -/** - * A pending message was set (upsert semantics: creates or replaces). - * - * For steering messages, this always replaces the single steering message. - * For queued messages, if a message with the given `id` already exists it is - * updated in place; otherwise it is appended to the queue. If the session is - * idle when a queued message is set, the server SHOULD immediately consume it - * and start a new turn. - * - * A client is only allowed to send {@link MessageKind.User} messages. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionPendingMessageSetAction { - type: ActionType.SessionPendingMessageSet; - /** Whether this is a steering or queued message */ - kind: PendingMessageKind; - /** Unique identifier for this pending message */ - id: string; - /** The message content */ - message: Message; -} - -/** - * A pending message was removed (steering or queued). - * - * Dispatched by clients to cancel a pending message, or by the server when - * it consumes a message (e.g. starting a turn from a queued message or - * injecting a steering message into the current turn). - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionPendingMessageRemovedAction { - type: ActionType.SessionPendingMessageRemoved; - /** Whether this is a steering or queued message */ - kind: PendingMessageKind; - /** Identifier of the pending message to remove */ - id: string; -} - -/** - * Reorder the queued messages. - * - * The `order` array contains the IDs of queued messages in their new - * desired order. IDs not present in the current queue are ignored. - * Queued messages whose IDs are absent from `order` are appended at - * the end in their original relative order (so a client with a stale - * view of the queue never silently drops messages). - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionQueuedMessagesReorderedAction { - type: ActionType.SessionQueuedMessagesReordered; - /** Queued message IDs in the desired order */ - order: string[]; -} - -// ─── Session Input Actions ────────────────────────────────────────────────── - -/** - * A session requested input from the user. - * - * Full-request upsert semantics: the `request` replaces any existing request - * with the same `id`, or is appended if it is new. Answer drafts are preserved - * unless `request.answers` is provided. - * - * @category Session Actions - * @version 1 - */ -export interface SessionInputRequestedAction { - type: ActionType.SessionInputRequested; - /** Input request to create or replace */ - request: SessionInputRequest; -} - -/** - * A client updated, submitted, skipped, or removed a single in-progress answer. - * - * Dispatching with `answer: undefined` removes that question's answer draft. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionInputAnswerChangedAction { - type: ActionType.SessionInputAnswerChanged; - /** Input request identifier */ - requestId: string; - /** Question identifier within the input request */ - questionId: string; - /** Updated answer, or `undefined` to clear an answer draft */ - answer?: SessionInputAnswer; -} - -/** - * A client accepted, declined, or cancelled a session input request. - * - * If accepted, the server uses `answers` (when provided) plus the request's - * synced answer state to resume the blocked operation. - * - * @category Session Actions - * @version 1 - * @clientDispatchable - */ -export interface SessionInputCompletedAction { - type: ActionType.SessionInputCompleted; - /** Input request identifier */ - requestId: string; - /** Completion outcome */ - response: SessionInputResponseKind; - /** Optional final answer replacement, keyed by question ID */ - answers?: Record; -} diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index 6731efc7..560d9b56 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -9,11 +9,13 @@ import type { URI } from '../common/state.js'; import type { BaseParams } from '../common/commands.js'; import type { ModelSelection } from '../channels-root/state.js'; import type { - Turn, SessionActiveClient, - MessageAttachment, AgentSelection, } from './state.js'; +import type { + Turn, + MessageAttachment, +} from '../channels-chat/state.js'; // ─── createSession ─────────────────────────────────────────────────────────── @@ -116,7 +118,7 @@ export interface DisposeSessionParams extends BaseParams {} // ─── fetchTurns ────────────────────────────────────────────────────────────── /** - * Fetches historical turns for a session. Used for lazy loading of conversation + * Fetches historical turns for a chat. Used for lazy loading of conversation * history. * * @category Commands @@ -128,7 +130,7 @@ export interface DisposeSessionParams extends BaseParams {} * ```jsonc * // Client → Server (fetch the 20 most recent turns) * { "jsonrpc": "2.0", "id": 8, "method": "fetchTurns", - * "params": { "channel": "ahp-session:/", "limit": 20 } } + * "params": { "channel": "ahp-chat:/", "limit": 20 } } * * // Server → Client * { "jsonrpc": "2.0", "id": 8, "result": { @@ -138,11 +140,11 @@ export interface DisposeSessionParams extends BaseParams {} * * // Client → Server (fetch 20 turns before t1) * { "jsonrpc": "2.0", "id": 9, "method": "fetchTurns", - * "params": { "channel": "ahp-session:/", "before": "t1", "limit": 20 } } + * "params": { "channel": "ahp-chat:/", "before": "t1", "limit": 20 } } * ``` */ export interface FetchTurnsParams extends BaseParams { - /** Session URI */ + /** Chat URI */ channel: URI; /** Turn ID to fetch before (exclusive). Omit to fetch from the most recent turn. */ before?: string; @@ -195,7 +197,7 @@ export const enum CompletionItemKind { * // User has typed "look at @foo" and the cursor is just after "@foo". * // Client → Server * { "jsonrpc": "2.0", "id": 12, "method": "completions", - * "params": { "kind": "userMessage", "channel": "ahp-session:/", + * "params": { "kind": "userMessage", "channel": "ahp-chat:/", * "text": "look at @foo", "offset": 12 } } * * // Server → Client @@ -219,7 +221,7 @@ export const enum CompletionItemKind { export interface CompletionsParams extends BaseParams { /** What kind of completion is being requested. */ kind: CompletionItemKind; - /** The session URI the completion is being requested for. */ + /** The chat URI the completion is being requested for. */ channel: URI; /** * The complete text of the input being completed (e.g. the full user diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index 49b04d01..8e902cb0 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -1,32 +1,17 @@ /** - * Session Channel Reducer — Pure reducer for `SessionState`, including all - * session-internal helpers (turn lifecycle, tool call transitions, pending - * messages, input requests). + * Session Channel Reducer — Pure reducer for `SessionState`. * * @module channels-session/reducer */ import { ActionType } from '../common/actions.js'; import type { - SessionInputRequest, SessionState, - ToolCallState, - ResponsePart, - ToolCallResponsePart, - Turn, - PendingMessage, - ConfirmationOption, McpServerCustomization, } from './state.js'; import { SessionLifecycle, SessionStatus, - TurnState, - ToolCallStatus, - ToolCallConfirmationReason, - ToolCallCancellationReason, - ResponsePartKind, - PendingMessageKind, CustomizationType, } from './state.js'; import type { SessionAction } from '../action-origin.generated.js'; @@ -34,235 +19,11 @@ import { softAssertNever } from '../common/reducer-helpers.js'; // ─── Helpers ───────────────────────────────────────────────────────────────── -/** Extracts the common base fields shared by all tool call lifecycle states. */ -function tcBase(tc: ToolCallState) { - return { - toolCallId: tc.toolCallId, - toolName: tc.toolName, - displayName: tc.displayName, - contributor: tc.contributor, - _meta: tc._meta, - }; -} - -function tcBaseWithMeta(tc: ToolCallState, meta: Record | undefined) { - return { - ...tcBase(tc), - _meta: meta ?? tc._meta, - }; -} - -/** Resolves a selected option from the confirmation options array by ID. */ -function resolveSelectedOption(options: ConfirmationOption[] | undefined, id: string | undefined): ConfirmationOption | undefined { - if (!id || !options) { - return undefined; - } - return options.find(o => o.id === id); -} - -/** Returns `true` if the active turn has any tool call awaiting user confirmation. */ -function hasPendingToolCallConfirmation(state: SessionState): boolean { - if (!state.activeTurn) { - return false; - } - return state.activeTurn.responseParts.some(part => - part.kind === ResponsePartKind.ToolCall - && (part.toolCall.status === ToolCallStatus.PendingConfirmation - || part.toolCall.status === ToolCallStatus.PendingResultConfirmation), - ); -} - -/** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */ -const STATUS_ACTIVITY_MASK = (1 << 5) - 1; - /** Sets or clears a metadata flag on a status value. */ function withStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean): SessionStatus { return set ? status | flag : status & ~flag; } -/** Derives the summary status from live session work, preserving orthogonal flags. */ -function summaryStatus(state: SessionState, terminalStatus?: SessionStatus.Error): SessionStatus { - let activity: SessionStatus; - if (terminalStatus) { - activity = terminalStatus; - } else if ((state.inputRequests?.length ?? 0) > 0 || hasPendingToolCallConfirmation(state)) { - activity = SessionStatus.InputNeeded; - } else if (state.activeTurn) { - activity = SessionStatus.InProgress; - } else { - activity = SessionStatus.Idle; - } - - return state.summary.status & ~STATUS_ACTIVITY_MASK | activity; -} - -/** - * Returns a state with `summary.status` recomputed. Use this after reducers - * that change data which feeds into {@link summaryStatus} (e.g. tool call - * lifecycle transitions that may enter or leave a pending-confirmation state). - */ -function refreshSummaryStatus(state: SessionState): SessionState { - const status = summaryStatus(state); - if (status === state.summary.status) { - return state; - } - return { ...state, summary: { ...state.summary, status } }; -} - -/** - * Ends the active turn, finalizing it into a completed turn record. - * - * Tool call parts with non-terminal states are forced to cancelled. - * Pending permissions are stripped from tool call parts. - */ -function endTurn( - state: SessionState, - turnId: string, - turnState: TurnState, - terminalStatus?: SessionStatus.Error, - error?: { errorType: string; message: string; stack?: string }, -): SessionState { - if (!state.activeTurn || state.activeTurn.id !== turnId) { - return state; - } - const active = state.activeTurn; - - const responseParts: ResponsePart[] = active.responseParts.map(part => { - if (part.kind !== ResponsePartKind.ToolCall) { - return part; - } - const tc = part.toolCall; - if (tc.status === ToolCallStatus.Completed || tc.status === ToolCallStatus.Cancelled) { - return part; - } - // Force non-terminal tool calls into cancelled state - return { - kind: ResponsePartKind.ToolCall, - toolCall: { - status: ToolCallStatus.Cancelled as const, - ...tcBase(tc), - invocationMessage: tc.status === ToolCallStatus.Streaming ? (tc.invocationMessage ?? '') : tc.invocationMessage, - toolInput: tc.status === ToolCallStatus.Streaming ? undefined : tc.toolInput, - reason: ToolCallCancellationReason.Skipped, - }, - }; - }); - - const turn: Turn = { - id: active.id, - message: active.message, - responseParts, - usage: active.usage, - state: turnState, - error, - }; - - const next: SessionState = { - ...state, - turns: [...state.turns, turn], - activeTurn: undefined, - summary: { ...state.summary, modifiedAt: Date.now() }, - }; - delete next.inputRequests; - return { - ...next, - summary: { ...next.summary, status: summaryStatus(next, terminalStatus) }, - }; -} - -function upsertInputRequest(state: SessionState, request: SessionInputRequest): SessionState { - const existing = state.inputRequests ?? []; - const idx = existing.findIndex(r => r.id === request.id); - const inputRequests = [...existing]; - if (idx >= 0) { - const answers = request.answers ?? inputRequests[idx].answers; - inputRequests[idx] = { ...request, answers }; - } else { - inputRequests.push(request); - } - const next = { ...state, inputRequests }; - return { ...next, summary: { ...next.summary, status: withStatusFlag(summaryStatus(next), SessionStatus.IsRead, false), modifiedAt: Date.now() } }; -} - -/** - * Immutably updates the tool call inside a `ToolCall` response part in the - * active turn's `responseParts` array. Returns `state` unchanged if the - * active turn or tool call doesn't match. - */ -function updateToolCallInParts( - state: SessionState, - turnId: string, - toolCallId: string, - updater: (tc: ToolCallState) => ToolCallState, -): SessionState { - const activeTurn = state.activeTurn; - if (!activeTurn || activeTurn.id !== turnId) { - return state; - } - - let found = false; - const responseParts = activeTurn.responseParts.map(part => { - if (part.kind === ResponsePartKind.ToolCall && part.toolCall.toolCallId === toolCallId) { - const updated = updater(part.toolCall); - if (updated === part.toolCall) { - return part; - } - found = true; - return { ...part, toolCall: updated }; - } - return part; - }); - - if (!found) { - return state; - } - - return { - ...state, - activeTurn: { ...activeTurn, responseParts }, - }; -} - -/** - * Immutably updates a response part by `partId` in the active turn. - * For markdown/reasoning parts, matches on `id`. For tool call parts, - * matches on `toolCall.toolCallId`. - */ -function updateResponsePart( - state: SessionState, - turnId: string, - partId: string, - updater: (part: ResponsePart) => ResponsePart, -): SessionState { - const activeTurn = state.activeTurn; - if (!activeTurn || activeTurn.id !== turnId) { - return state; - } - - let found = false; - const responseParts = activeTurn.responseParts.map(part => { - if (!found) { - const id = part.kind === ResponsePartKind.ToolCall - ? part.toolCall.toolCallId - : 'id' in part ? part.id : undefined; - if (id === partId) { - found = true; - return updater(part); - } - } - return part; - }); - - if (!found) { - return state; - } - - return { - ...state, - activeTurn: { ...activeTurn, responseParts }, - }; -} - // ─── Session Reducer ───────────────────────────────────────────────────────── /** @@ -290,238 +51,46 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: creationError: action.error, }; - // ── Turn Lifecycle ──────────────────────────────────────────────────── - - case ActionType.SessionTurnStarted: { - let next: SessionState = { - ...state, - activeTurn: { - id: action.turnId, - message: action.message, - responseParts: [], - usage: undefined, - }, - }; - next = { - ...next, - summary: { ...next.summary, status: withStatusFlag(summaryStatus(next), SessionStatus.IsRead, false), modifiedAt: Date.now() }, - }; - - // If this turn was auto-started from a pending message, remove it - if (action.queuedMessageId) { - if (next.steeringMessage?.id === action.queuedMessageId) { - next = { ...next, steeringMessage: undefined }; - } - if (next.queuedMessages) { - const filtered = next.queuedMessages.filter(m => m.id !== action.queuedMessageId); - next = { ...next, queuedMessages: filtered.length > 0 ? filtered : undefined }; - } + case ActionType.SessionChatAdded: { + const list = state.chats; + const idx = list.findIndex(c => c.resource === action.summary.resource); + if (idx < 0) { + return { ...state, chats: [...list, action.summary] }; } - - return next; + const updated = list.slice(); + updated[idx] = action.summary; + return { ...state, chats: updated }; } - case ActionType.SessionDelta: - return updateResponsePart(state, action.turnId, action.partId, part => { - if (part.kind === ResponsePartKind.Markdown) { - return { ...part, content: part.content + action.content }; - } - return part; - }); - - case ActionType.SessionResponsePart: - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + case ActionType.SessionChatRemoved: { + const list = state.chats; + const idx = list.findIndex(c => c.resource === action.chat); + if (idx < 0) { return state; } - return { - ...state, - activeTurn: { - ...state.activeTurn, - responseParts: [...state.activeTurn.responseParts, action.part], - }, - }; - - case ActionType.SessionTurnComplete: - return endTurn(state, action.turnId, TurnState.Complete); - - case ActionType.SessionTurnCancelled: - return endTurn(state, action.turnId, TurnState.Cancelled); - - case ActionType.SessionError: - return endTurn(state, action.turnId, TurnState.Error, SessionStatus.Error, action.error); - - // ── Tool Call State Machine ─────────────────────────────────────────── + const updated = list.slice(); + updated.splice(idx, 1); + const next: SessionState = { ...state, chats: updated }; + if (state.defaultChat === action.chat) { + delete next.defaultChat; + } + return next; + } - case ActionType.SessionToolCallStart: - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + case ActionType.SessionChatUpdated: { + const list = state.chats; + const idx = list.findIndex(c => c.resource === action.chat); + if (idx < 0) { return state; } - return { - ...state, - activeTurn: { - ...state.activeTurn, - responseParts: [ - ...state.activeTurn.responseParts, - { - kind: ResponsePartKind.ToolCall, - toolCall: { - toolCallId: action.toolCallId, - toolName: action.toolName, - displayName: action.displayName, - contributor: action.contributor, - _meta: action._meta, - status: ToolCallStatus.Streaming, - }, - } satisfies ToolCallResponsePart, - ], - }, - }; - - case ActionType.SessionToolCallDelta: - return updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { - if (tc.status !== ToolCallStatus.Streaming) { - return tc; - } - return { - ...tc, - ...(action._meta !== undefined ? { _meta: action._meta } : {}), - partialInput: (tc.partialInput ?? '') + action.content, - invocationMessage: action.invocationMessage ?? tc.invocationMessage, - }; - }); - - case ActionType.SessionToolCallReady: - return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { - if (tc.status !== ToolCallStatus.Streaming && tc.status !== ToolCallStatus.Running) { - return tc; - } - const base = tcBaseWithMeta(tc, action._meta); - if (action.confirmed) { - return { - status: ToolCallStatus.Running, - ...base, - invocationMessage: action.invocationMessage, - toolInput: action.toolInput, - confirmed: action.confirmed, - }; - } - return { - status: ToolCallStatus.PendingConfirmation, - ...base, - invocationMessage: action.invocationMessage, - toolInput: action.toolInput, - confirmationTitle: action.confirmationTitle, - edits: action.edits, - editable: action.editable, - ...(action.options ? { options: action.options } : {}), - }; - })); - - case ActionType.SessionToolCallConfirmed: - return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { - if (tc.status !== ToolCallStatus.PendingConfirmation) { - return tc; - } - const base = tcBaseWithMeta(tc, action._meta); - const selectedOption = resolveSelectedOption(tc.options, action.selectedOptionId); - if (action.approved) { - return { - status: ToolCallStatus.Running, - ...base, - invocationMessage: tc.invocationMessage, - toolInput: action.editedToolInput ?? tc.toolInput, - confirmed: action.confirmed, - ...(selectedOption ? { selectedOption } : {}), - }; - } - return { - status: ToolCallStatus.Cancelled, - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - reason: action.reason, - reasonMessage: action.reasonMessage, - userSuggestion: action.userSuggestion, - ...(selectedOption ? { selectedOption } : {}), - }; - })); - - case ActionType.SessionToolCallComplete: - return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { - if (tc.status !== ToolCallStatus.Running && tc.status !== ToolCallStatus.PendingConfirmation) { - return tc; - } - const base = tcBaseWithMeta(tc, action._meta); - const confirmed = tc.status === ToolCallStatus.Running - ? tc.confirmed - : ToolCallConfirmationReason.NotNeeded; - const selectedOption = tc.status === ToolCallStatus.Running - ? tc.selectedOption - : undefined; - if (action.requiresResultConfirmation) { - return { - status: ToolCallStatus.PendingResultConfirmation, - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - confirmed, - ...(selectedOption ? { selectedOption } : {}), - ...action.result, - }; - } - return { - status: ToolCallStatus.Completed, - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - confirmed, - ...(selectedOption ? { selectedOption } : {}), - ...action.result, - }; - })); - - case ActionType.SessionToolCallResultConfirmed: - return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { - if (tc.status !== ToolCallStatus.PendingResultConfirmation) { - return tc; - } - const base = tcBaseWithMeta(tc, action._meta); - if (action.approved) { - return { - status: ToolCallStatus.Completed, - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - confirmed: tc.confirmed, - ...(tc.selectedOption ? { selectedOption: tc.selectedOption } : {}), - success: tc.success, - pastTenseMessage: tc.pastTenseMessage, - content: tc.content, - structuredContent: tc.structuredContent, - error: tc.error, - }; - } - return { - status: ToolCallStatus.Cancelled, - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - reason: ToolCallCancellationReason.ResultDenied, - ...(tc.selectedOption ? { selectedOption: tc.selectedOption } : {}), - }; - })); + const { resource: _ignored, ...changes } = action.changes; + const updated = list.slice(); + updated[idx] = { ...list[idx], ...changes }; + return { ...state, chats: updated }; + } - case ActionType.SessionToolCallContentChanged: - return updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { - if (tc.status !== ToolCallStatus.Running) { - return tc; - } - return { - ...tc, - ...(action._meta !== undefined ? { _meta: action._meta } : {}), - content: action.content, - }; - }); + case ActionType.SessionDefaultChatChanged: + return { ...state, defaultChat: action.defaultChat }; // ── Metadata ────────────────────────────────────────────────────────── @@ -531,23 +100,6 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: summary: { ...state.summary, title: action.title, modifiedAt: Date.now() }, }; - case ActionType.SessionUsage: - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - return { - ...state, - activeTurn: { ...state.activeTurn, usage: action.usage }, - }; - - case ActionType.SessionReasoning: - return updateResponsePart(state, action.turnId, action.partId, part => { - if (part.kind === ResponsePartKind.Reasoning) { - return { ...part, content: part.content + action.content }; - } - return part; - }); - case ActionType.SessionModelChanged: return { ...state, @@ -740,141 +292,6 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: return { ...state, customizations: updated }; } - // ── Truncation ──────────────────────────────────────────────────────── - - case ActionType.SessionTruncated: { - let turns: typeof state.turns; - if (action.turnId === undefined) { - turns = []; - } else { - const idx = state.turns.findIndex(t => t.id === action.turnId); - if (idx < 0) { - return state; - } - turns = state.turns.slice(0, idx + 1); - } - const next: SessionState = { - ...state, - turns, - activeTurn: undefined, - summary: { ...state.summary, modifiedAt: Date.now() }, - }; - delete next.inputRequests; - return { - ...next, - summary: { ...next.summary, status: summaryStatus(next) }, - }; - } - - // ── Session Input Requests ───────────────────────────────────────────── - - case ActionType.SessionInputRequested: - return upsertInputRequest(state, action.request); - - case ActionType.SessionInputAnswerChanged: { - const existing = state.inputRequests; - const idx = existing?.findIndex(request => request.id === action.requestId) ?? -1; - if (!existing || idx < 0) { - return state; - } - const request = existing[idx]; - const answers = { ...(request.answers ?? {}) }; - if (action.answer === undefined) { - delete answers[action.questionId]; - } else { - answers[action.questionId] = action.answer; - } - const updated = [...existing]; - updated[idx] = { - ...request, - answers: Object.keys(answers).length > 0 ? answers : undefined, - }; - return { - ...state, - inputRequests: updated, - summary: { ...state.summary, modifiedAt: Date.now() }, - }; - } - - case ActionType.SessionInputCompleted: { - const existing = state.inputRequests; - if (!existing?.some(request => request.id === action.requestId)) { - return state; - } - const inputRequests = existing.filter(request => request.id !== action.requestId); - const next: SessionState = { - ...state, - }; - if (inputRequests.length > 0) { - next.inputRequests = inputRequests; - } else { - delete next.inputRequests; - } - return { - ...next, - summary: { ...next.summary, status: summaryStatus(next), modifiedAt: Date.now() }, - }; - } - - // ── Pending Messages ────────────────────────────────────────────────── - - case ActionType.SessionPendingMessageSet: { - const entry: PendingMessage = { id: action.id, message: action.message }; - if (action.kind === PendingMessageKind.Steering) { - return { ...state, steeringMessage: entry }; - } - const existing = state.queuedMessages ?? []; - const idx = existing.findIndex(m => m.id === action.id); - if (idx >= 0) { - const updated = [...existing]; - updated[idx] = entry; - return { ...state, queuedMessages: updated }; - } - return { ...state, queuedMessages: [...existing, entry] }; - } - - case ActionType.SessionPendingMessageRemoved: { - if (action.kind === PendingMessageKind.Steering) { - if (!state.steeringMessage || state.steeringMessage.id !== action.id) { - return state; - } - return { ...state, steeringMessage: undefined }; - } - const existing = state.queuedMessages; - if (!existing) { - return state; - } - const filtered = existing.filter(m => m.id !== action.id); - return filtered.length === existing.length - ? state - : { ...state, queuedMessages: filtered.length > 0 ? filtered : undefined }; - } - - case ActionType.SessionQueuedMessagesReordered: { - const existing = state.queuedMessages; - if (!existing) { - return state; - } - const byId = new Map(existing.map(m => [m.id, m])); - const ordered = new Set(); - const reordered = action.order - .filter(id => { - if (byId.has(id) && !ordered.has(id)) { - ordered.add(id); - return true; - } - return false; - }) - .map(id => byId.get(id)!); - // Append any messages not mentioned in order, preserving original order - for (const m of existing) { - if (!ordered.has(m.id)) { - reordered.push(m); - } - } - return { ...state, queuedMessages: reordered }; - } - default: softAssertNever(action, log); return state; diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index dd38b953..48a5ca6f 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -1,57 +1,22 @@ /** - * Session State Types — Per-session state, turns, messages, response parts, - * tool calls, and elicitation/input requests exposed on `ahp-session:` channels. + * Session State Types — Per-session coordination state exposed on `ahp-session:` channels. * * @module channels-session/state */ 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 { ModelSelection } from '../channels-root/state.js'; import type { ConfigPropertySchema, - ContentRef, ErrorInfo, - FileEdit, Icon, ProtectedResourceMetadata, - StringOrMarkdown, TextRange, - TextSelection, URI, - UsageInfo, } from '../common/state.js'; -// ─── Pending Message Types ─────────────────────────────────────────────────── - -/** - * Discriminant for pending message kinds. - * - * @category Pending Message Types - */ -export const enum PendingMessageKind { - /** Injected into the current turn at a convenient point */ - Steering = 'steering', - /** Sent automatically as a new turn after the current turn finishes */ - Queued = 'queued', -} - -/** - * A message queued for future delivery to the agent. - * - * Steering messages are injected into the current turn mid-flight. - * Queued messages are automatically started as new turns after the - * current turn naturally finishes. - * - * @category Pending Message Types - */ -export interface PendingMessage { - /** Unique identifier for this pending message */ - id: string; - /** The message that will start the next turn */ - message: Message; -} - // ─── Session State ─────────────────────────────────────────────────────────── /** @@ -105,16 +70,15 @@ export interface SessionState { serverTools?: ToolDefinition[]; /** The client currently providing tools and interactive capabilities to this session */ activeClient?: SessionActiveClient; - /** Completed turns */ - turns: Turn[]; - /** Currently in-progress turn */ - activeTurn?: ActiveTurn; - /** Message to inject into the current turn at a convenient point */ - steeringMessage?: PendingMessage; - /** Messages to send automatically as new turns after the current turn finishes */ - queuedMessages?: PendingMessage[]; - /** Requests for user input that are currently blocking or informing session progress */ - inputRequests?: SessionInputRequest[]; + /** Catalog of chats in this session. */ + chats: ChatSummary[]; + /** + * The chat that receives input when the user addresses the session without + * selecting a specific chat. This is a UI routing hint, not a hierarchy + * marker — chats remain equal peers at the protocol level. Hosts MAY change + * this over the session's lifetime. + */ + defaultChat?: URI; /** Session configuration schema and current values */ config?: SessionConfigState; /** @@ -196,6 +160,40 @@ export interface ProjectInfo { } /** + * Lightweight catalog entry summarizing one session. Surfaced via + * {@link RootChannelCommands.listSessions | `root/listSessions`} and + * `root/sessionAdded`/`root/sessionSummaryChanged` notifications. + * + * **Aggregation across chats.** Once a session contains more than one chat, + * several `SessionSummary` fields are derived from the underlying + * {@link SessionState.chats | chat catalog}. Producers SHOULD follow these + * rules so clients that only consume the session summary (e.g. a session + * list) still see meaningful state: + * + * - `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` / + * `Error` — bits 0–4) from the + * {@link SessionState.defaultChat | default chat} when present, else from + * the most recently modified chat. **Promote** `InputNeeded` whenever any + * chat in the session needs input, and **promote** `Error` whenever any + * chat is in an error state — both override the default-chat bits. The + * orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped. + * - `activity`: mirror the activity string of the default chat, or of the + * chat currently driving the promoted status bits when a non-default chat + * wins (e.g. the chat that raised `InputNeeded`). + * - `modifiedAt`: the max of all chats' `modifiedAt`. + * - `model` / `agent`: the session-level selection. Per-chat overrides are + * surfaced on individual {@link ChatSummary} entries, not aggregated up. + * - `workingDirectory`: the session-level **default**. Individual chats MAY + * override via {@link ChatSummary.workingDirectory}; aggregating these up + * is meaningless and SHOULD NOT be attempted. + * - `changes`: optional roll-up across all chats. Producers MAY sum the + * per-chat changeset stats or report the most expensive chat's stats — + * whichever is cheaper for the host to compute. + * + * Sessions with a single chat trivially satisfy all of the above (the chat's + * values pass through unchanged). The rules only matter once a session + * carries multiple chats. + * * @category Session State */ export interface SessionSummary { @@ -224,7 +222,12 @@ export interface SessionSummary { * — the session uses the provider's default behavior. */ agent?: AgentSelection; - /** The working directory URI for this session */ + /** + * The default working directory URI for this session. Individual chats + * MAY override via {@link ChatSummary.workingDirectory | their own + * `workingDirectory`}; this field acts as the fallback for any chat that + * does not. + */ workingDirectory?: URI; /** * Aggregate summary of file changes associated with this session. Servers @@ -333,875 +336,6 @@ export interface SessionConfigState { values: Record; } -// ─── Session Input Types ──────────────────────────────────────────────────── - -/** - * How a client completed an input request. - * - * @category Session Input Types - */ -export const enum SessionInputResponseKind { - Accept = 'accept', - Decline = 'decline', - Cancel = 'cancel', -} - -/** - * Question/input control kind. - * - * @category Session Input Types - */ -export const enum SessionInputQuestionKind { - Text = 'text', - Number = 'number', - Integer = 'integer', - Boolean = 'boolean', - SingleSelect = 'single-select', - MultiSelect = 'multi-select', -} - -/** - * A choice in a select-style question. - * - * @category Session Input Types - */ -export interface SessionInputOption { - /** Stable option identifier; for MCP enum values this is the enum string */ - id: string; - /** Display label */ - label: string; - /** Optional secondary text */ - description?: string; - /** Whether this option is the recommended/default choice */ - recommended?: boolean; -} - -interface SessionInputQuestionBase { - /** Stable question identifier used as the key in `answers` */ - id: string; - /** Short display title */ - title?: string; - /** Prompt shown to the user */ - message: string; - /** Whether the user must answer this question to accept the request */ - required?: boolean; -} - -/** Text question within a session input request. */ -export interface SessionInputTextQuestion extends SessionInputQuestionBase { - kind: SessionInputQuestionKind.Text; - /** Format hint for text questions, such as `email`, `uri`, `date`, or `date-time` */ - format?: string; - /** Minimum string length */ - min?: number; - /** Maximum string length */ - max?: number; - /** Default text */ - defaultValue?: string; -} - -/** Numeric question within a session input request. */ -export interface SessionInputNumberQuestion extends SessionInputQuestionBase { - kind: SessionInputQuestionKind.Number | SessionInputQuestionKind.Integer; - /** - * Minimum value - * @format float - */ - min?: number; - /** - * Maximum value - * @format float - */ - max?: number; - /** - * Default numeric value - * @format float - */ - defaultValue?: number; -} - -/** Boolean question within a session input request. */ -export interface SessionInputBooleanQuestion extends SessionInputQuestionBase { - kind: SessionInputQuestionKind.Boolean; - /** Default boolean value */ - defaultValue?: boolean; -} - -/** Single-select question within a session input request. */ -export interface SessionInputSingleSelectQuestion extends SessionInputQuestionBase { - kind: SessionInputQuestionKind.SingleSelect; - /** Options the user may select from */ - options: SessionInputOption[]; - /** Whether the user may enter text instead of selecting an option */ - allowFreeformInput?: boolean; -} - -/** Multi-select question within a session input request. */ -export interface SessionInputMultiSelectQuestion extends SessionInputQuestionBase { - kind: SessionInputQuestionKind.MultiSelect; - /** Options the user may select from */ - options: SessionInputOption[]; - /** Whether the user may enter text in addition to selecting options */ - allowFreeformInput?: boolean; - /** Minimum selected item count */ - min?: number; - /** Maximum selected item count */ - max?: number; -} - -/** - * One question within a session input request. - * - * @category Session Input Types - */ -export type SessionInputQuestion = SessionInputTextQuestion - | SessionInputNumberQuestion - | SessionInputBooleanQuestion - | SessionInputSingleSelectQuestion - | SessionInputMultiSelectQuestion; - -/** - * A live request for user input. - * - * The server creates or replaces requests with `session/inputRequested`. - * Clients sync drafts with `session/inputAnswerChanged` and complete requests - * with `session/inputCompleted`. - * - * @category Session Input Types - */ -export interface SessionInputRequest { - /** Stable request identifier */ - id: string; - /** Display message for the request as a whole */ - message?: string; - /** URL the user should review or open, for URL-style elicitations */ - url?: URI; - /** Ordered questions to ask the user */ - questions?: SessionInputQuestion[]; - /** Current draft or submitted answers, keyed by question ID */ - answers?: Record; -} - -/** - * Answer value kind. - * - * @category Session Input Types - */ -export const enum SessionInputAnswerValueKind { - Text = 'text', - Number = 'number', - Boolean = 'boolean', - Selected = 'selected', - SelectedMany = 'selected-many', -} - -/** - * Value captured for one answer. - * - * @category Session Input Types - */ -export interface SessionInputTextAnswerValue { - kind: SessionInputAnswerValueKind.Text; - value: string; -} - -export interface SessionInputNumberAnswerValue { - kind: SessionInputAnswerValueKind.Number; - /** @format float */ - value: number; -} - -export interface SessionInputBooleanAnswerValue { - kind: SessionInputAnswerValueKind.Boolean; - value: boolean; -} - -export interface SessionInputSelectedAnswerValue { - kind: SessionInputAnswerValueKind.Selected; - value: string; - /** Free-form text entered instead of selecting an option */ - freeformValues?: string[]; -} - -export interface SessionInputSelectedManyAnswerValue { - kind: SessionInputAnswerValueKind.SelectedMany; - value: string[]; - /** Free-form text entered in addition to selected options */ - freeformValues?: string[]; -} - -export type SessionInputAnswerValue = SessionInputTextAnswerValue - | SessionInputNumberAnswerValue - | SessionInputBooleanAnswerValue - | SessionInputSelectedAnswerValue - | SessionInputSelectedManyAnswerValue; - -export interface SessionInputAnswered { - /** Answer state */ - state: SessionInputAnswerState.Draft | SessionInputAnswerState.Submitted; - /** Answer value */ - value: SessionInputAnswerValue; -} - -export interface SessionInputSkipped { - /** Answer state */ - state: SessionInputAnswerState.Skipped; - /** Free-form reason or value captured while skipping, if any */ - freeformValues?: string[]; -} - -/** - * Answer lifecycle state. - * - * @category Session Input Types - */ -export const enum SessionInputAnswerState { - Draft = 'draft', - Submitted = 'submitted', - Skipped = 'skipped', -} - -/** - * Draft, submitted, or skipped answer for one question. - * - * @category Session Input Types - */ -export type SessionInputAnswer = SessionInputAnswered | SessionInputSkipped; - -// ─── Turn Types ────────────────────────────────────────────────────────────── - -/** - * How a turn ended. - * - * @category Turn Types - */ -export const enum TurnState { - Complete = 'complete', - Cancelled = 'cancelled', - Error = 'error', -} - -/** - * Discriminant for {@link MessageAttachment} variants. - * - * @category Turn Types - */ -export const enum MessageAttachmentKind { - /** A simple, opaque attachment whose representation is described by the producer. */ - Simple = 'simple', - /** An attachment whose data is embedded inline as a base64 string. */ - EmbeddedResource = 'embeddedResource', - /** An attachment that references a resource by URI. */ - Resource = 'resource', - /** An attachment that references annotations on an annotations channel. */ - Annotations = 'annotations', -} - -/** - * A completed request/response cycle. - * - * @category Turn Types - */ -export interface Turn { - /** Turn identifier */ - id: string; - /** The message that initiated the turn */ - message: Message; - /** - * All response content in stream order: text, tool calls, reasoning, and content refs. - * - * Consumers should derive display text by concatenating markdown parts, - * and find tool calls by filtering for `ToolCall` parts. - */ - responseParts: ResponsePart[]; - /** Token usage info */ - usage: UsageInfo | undefined; - /** How the turn ended */ - state: TurnState; - /** Error details if state is `'error'` */ - error?: ErrorInfo; -} - -/** - * An in-progress turn — the assistant is actively streaming. - * - * @category Turn Types - */ -export interface ActiveTurn { - /** Turn identifier */ - id: string; - /** The message that initiated the turn */ - message: Message; - /** - * All response content in stream order: text, tool calls, reasoning, and content refs. - * - * Tool call parts include `pendingPermissions` when permissions are awaiting user approval. - */ - responseParts: ResponsePart[]; - /** Token usage info */ - usage: UsageInfo | undefined; -} - -/** - * Discriminant for Message types. - * - * @category Turn Types - */ -export enum MessageKind { - User = 'user', - SystemNotification = 'systemNotification', -} - -/** - * A message that initiates or steers a turn. Messages can originate from the - * user or be system-generated (see {@link MessageKind}). - * - * Attachments MAY be referenced inside {@link Message.text} via their - * {@link MessageAttachmentBase.range} field. Attachments without a range are - * still associated with the message but do not correspond to a specific span - * in the text. - * - * @category Turn Types - */ -export interface Message { - /** Message text */ - text: string; - /** The origin of the message */ - origin: { kind: MessageKind }; - /** File/selection attachments */ - attachments?: MessageAttachment[]; - /** - * Additional provider-specific metadata for this message. - * - * Clients MAY look for well-known keys here to provide enhanced UI, and - * agent hosts MAY use it to carry context that does not fit any other - * field. Mirrors the MCP `_meta` convention. - */ - _meta?: Record; -} - -/** - * Common fields shared by all {@link MessageAttachment} variants. - * - * @category Turn Types - */ -export interface MessageAttachmentBase { - /** - * A human-readable label for the attachment (e.g. the filename of a file - * attachment). Used for display in UI. - */ - label: string; - - /** - * If defined, the range in {@link Message.text} that references this - * attachment. This is a text range, not a byte range. - */ - range?: TextRange; - - /** - * Advisory display hint for clients rendering this attachment. Recognized - * values include: - * - * - `'image'`: the attachment is an image - * - `'document'`: the attachment is a textual document - * - `'symbol'`: the attachment is a code symbol (e.g. a function or class) - * - `'directory'`: the attachment is a folder - * - `'selection'`: the attachment is a selection within a document - * - * Implementations MAY provide additional values; clients SHOULD fall back - * to a reasonable default when an unknown value is encountered. - */ - displayKind?: string; - - /** - * Additional implementation-defined metadata for the attachment. - * - * If the attachment was produced by the `completions` command, the client - * MUST preserve every property of `_meta` originally returned by the agent - * host when sending the user message containing the accepted completion. - */ - _meta?: Record; -} - -/** - * A simple, opaque attachment whose model representation is described by - * the producer. - * - * @category Turn Types - */ -export interface SimpleMessageAttachment extends MessageAttachmentBase { - /** Discriminant */ - type: MessageAttachmentKind.Simple; - - /** - * Representation of the attachment as it should be shown to the model. - * - * If the attachment was produced by the client, this property MUST be - * defined so the agent host can correctly interpret the attachment. This - * property MAY be omitted when the attachment originated from a - * `completions` response. - */ - modelRepresentation?: string; -} - -/** - * An attachment whose data is embedded inline as a base64 string. - * - * Use this for small binary payloads (e.g. a pasted image) that should be - * delivered with the user message itself rather than fetched separately. - * - * @category Turn Types - */ -export interface MessageEmbeddedResourceAttachment extends MessageAttachmentBase { - /** Discriminant */ - type: MessageAttachmentKind.EmbeddedResource; - /** Base64-encoded binary data */ - data: string; - /** Content MIME type (e.g. `"image/png"`, `"application/pdf"`) */ - contentType: string; - /** - * Optional selection within the attached textual resource. - * - * Only meaningful for textual resources. - */ - selection?: TextSelection; -} - -/** - * An attachment that references a resource by URI. The content is not - * delivered inline; consumers can fetch it via `resourceRead` when needed. - * - * @category Turn Types - */ -export interface MessageResourceAttachment extends MessageAttachmentBase, ContentRef { - /** Discriminant */ - type: MessageAttachmentKind.Resource; - /** - * Optional selection within the referenced textual resource. - * - * Only meaningful for textual resources. - */ - selection?: TextSelection; -} - -/** - * An attachment that references annotations on a session's annotations - * channel (see {@link AnnotationsState}). - * - * When {@link annotationIds} is omitted the attachment references every - * annotation on the channel; when present it references only the listed - * {@link Annotation.id | annotation ids}. - * - * @category Turn Types - */ -export interface MessageAnnotationsAttachment extends MessageAttachmentBase { - /** Discriminant */ - type: MessageAttachmentKind.Annotations; - /** - * The annotations channel URI (typically `ahp-session://annotations`). - * Matches {@link AnnotationsSummary.resource}. - */ - resource: URI; - /** - * Specific {@link Annotation.id | annotation ids} to reference. When - * omitted, the attachment references all annotations on the channel. - */ - annotationIds?: string[]; -} - -/** - * An attachment associated with a {@link Message}. - * - * @category Turn Types - */ -export type MessageAttachment = - | SimpleMessageAttachment - | MessageEmbeddedResourceAttachment - | MessageResourceAttachment - | MessageAnnotationsAttachment; - -// ─── Response Parts ────────────────────────────────────────────────────────── - -/** - * Discriminant for response part types. - * - * @category Response Parts - */ -export const enum ResponsePartKind { - Markdown = 'markdown', - ContentRef = 'contentRef', - ToolCall = 'toolCall', - Reasoning = 'reasoning', - SystemNotification = 'systemNotification', -} - -/** - * @category Response Parts - */ -export interface MarkdownResponsePart { - /** Discriminant */ - kind: ResponsePartKind.Markdown; - /** Part identifier, used by `session/delta` to target this part for content appends */ - id: string; - /** Markdown content */ - content: string; -} - -/** - * A content part that's a reference to large content stored outside the state tree. - * - * @category Response Parts - */ -export interface ResourceReponsePart extends ContentRef { - /** Discriminant */ - kind: ResponsePartKind.ContentRef; -} - -/** - * A tool call represented as a response part. - * - * Tool calls are part of the response stream, interleaved with text and - * reasoning. The `toolCall.toolCallId` serves as the part identifier for - * actions that target this part. - * - * @category Response Parts - */ -export interface ToolCallResponsePart { - /** Discriminant */ - kind: ResponsePartKind.ToolCall; - /** Full tool call lifecycle state */ - toolCall: ToolCallState; -} - -/** - * Reasoning/thinking content from the model. - * - * @category Response Parts - */ -export interface ReasoningResponsePart { - /** Discriminant */ - kind: ResponsePartKind.Reasoning; - /** Part identifier, used by `session/reasoning` to target this part for content appends */ - id: string; - /** Accumulated reasoning text */ - content: string; -} - -/** - * @category Response Parts - */ -export type ResponsePart = - | MarkdownResponsePart - | ResourceReponsePart - | ToolCallResponsePart - | ReasoningResponsePart - | SystemNotificationResponsePart; - -/** - * A system notification surfaced as part of the response stream. - * - * System notifications are messages authored by the agent harness - * that need to be visible to both the agent (for situational awareness) and - * the user (for transcript continuity). Examples include "background subagent - * X completed" or "task Y was cancelled". - * - * @category Response Parts - */ -export interface SystemNotificationResponsePart { - /** Discriminant */ - kind: ResponsePartKind.SystemNotification; - /** The text of the system notification */ - content: StringOrMarkdown; -} - - -// ─── Tool Call Types ───────────────────────────────────────────────────────── - -/** - * Status of a tool call in the lifecycle state machine. - * - * @category Tool Call Types - */ -export const enum ToolCallStatus { - Streaming = 'streaming', - PendingConfirmation = 'pending-confirmation', - Running = 'running', - PendingResultConfirmation = 'pending-result-confirmation', - Completed = 'completed', - Cancelled = 'cancelled', -} - -/** - * How a tool call was confirmed for execution. - * - * - `NotNeeded` — No confirmation required (auto-approved) - * - `UserAction` — User explicitly approved - * - `Setting` — Approved by a persistent user setting - * - * @category Tool Call Types - */ -export const enum ToolCallConfirmationReason { - NotNeeded = 'not-needed', - UserAction = 'user-action', - Setting = 'setting', -} - -/** - * Why a tool call was cancelled. - * - * @category Tool Call Types - */ -export const enum ToolCallCancellationReason { - Denied = 'denied', - Skipped = 'skipped', - ResultDenied = 'result-denied', -} - -/** - * Whether a confirmation option represents an approval or denial action. - * - * @category Tool Call Types - */ -export const enum ConfirmationOptionKind { - Approve = 'approve', - Deny = 'deny', -} - -/** - * A confirmation option that the server offers for a tool call awaiting - * approval. Allows richer choices beyond simple approve/deny — for example, - * "Approve in this Session" or "Deny with reason." - * - * @category Tool Call Types - */ -export interface ConfirmationOption { - /** Unique identifier for the option, returned in the confirmed action */ - id: string; - /** Human-readable label displayed to the user */ - label: string; - /** Whether this option represents an approval or denial */ - kind: ConfirmationOptionKind; - /** - * Logical group number for visual categorisation. - * - * Clients SHOULD display options in the order they are defined and MAY - * use differing group numbers to insert dividers between logical clusters - * of options. - */ - group?: number; -} - -export const enum ToolCallContributorKind { - Client = 'client', - MCP = 'mcp', -} - -export interface ToolCallClientContributor { - kind: ToolCallContributorKind.Client; - /** - * If this tool is provided by a client, the `clientId` of the owning client. - * Absent for server-side tools. - * - * When set, the identified client is responsible for executing the tool and - * dispatching `session/toolCallComplete` with the result. - */ - clientId: string; -} - -export interface ToolCallMcpContributor { - kind: ToolCallContributorKind.MCP; - /** - * Customization ID of the corresponding MCP server in {@link SessionState.customizations}. - */ - customizationId: string; -} - -export type ToolCallContributor = ToolCallClientContributor | ToolCallMcpContributor; - -/** - * Metadata common to all tool call states. - * - * @category Tool Call Types - * @remarks - * Fields like `toolName` carry agent-specific identifiers on the wire despite the - * agent-agnostic design principle. These exist for debugging and logging purposes. - * A future version may move these to a separate diagnostic channel or namespace them - * more clearly. - */ -interface ToolCallBase { - /** Unique tool call identifier */ - toolCallId: string; - /** Internal tool name (for debugging/logging) */ - toolName: string; - /** Human-readable tool name */ - displayName: string; - /** - * Reference to the contributor of the tool being called. - */ - contributor?: ToolCallContributor; - /** - * Additional provider-specific metadata for this tool call. - * - * This MAY include a `ui` field corresponding to the MCP Apps (SEP-1865) - * `McpUiToolMeta` found in MCP tool calls, which may be used in combination - * with the {@link contributor} to serve MCP Apps. - */ - _meta?: Record; -} - -/** - * Properties available once tool call parameters are fully received. - * - * @category Tool Call Types - */ -interface ToolCallParameterFields { - /** Message describing what the tool will do */ - invocationMessage: StringOrMarkdown; - /** Raw tool input */ - toolInput?: string; -} - -/** - * Tool execution result details, available after execution completes. - * - * @category Tool Call Types - */ -export interface ToolCallResult { - /** Whether the tool succeeded */ - success: boolean; - /** Past-tense description of what the tool did */ - pastTenseMessage: StringOrMarkdown; - /** - * Unstructured result content blocks. - * - * This mirrors the `content` field of MCP `CallToolResult`. - */ - content?: ToolResultContent[]; - /** - * Optional structured result object. - * - * This mirrors the `structuredContent` field of MCP `CallToolResult`. - */ - structuredContent?: Record; - /** Error details if the tool failed */ - error?: { message: string; code?: string }; -} - -/** - * LM is streaming the tool call parameters. - * - * @category Tool Call Types - */ -export interface ToolCallStreamingState extends ToolCallBase { - status: ToolCallStatus.Streaming; - /** Partial parameters accumulated so far */ - partialInput?: string; - /** Progress message shown while parameters are streaming */ - invocationMessage?: StringOrMarkdown; -} - -/** - * Parameters are complete, or a running tool requires re-confirmation - * (e.g. a mid-execution permission check). - * - * @category Tool Call Types - */ -export interface ToolCallPendingConfirmationState extends ToolCallBase, ToolCallParameterFields { - status: ToolCallStatus.PendingConfirmation; - /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ - confirmationTitle?: StringOrMarkdown; - /** File edits that this tool call will perform, for preview before confirmation */ - edits?: { items: FileEdit[] }; - /** Whether the agent host allows the client to edit the tool's input parameters before confirming */ - editable?: boolean; - /** - * Options the server offers for this confirmation. When present, the client - * SHOULD render these instead of a plain approve/deny UI. Each option - * belongs to a {@link ConfirmationOptionGroup} so the client can still - * categorise the choices. - */ - options?: ConfirmationOption[]; -} - -/** - * Tool is actively executing. - * - * @category Tool Call Types - */ -export interface ToolCallRunningState extends ToolCallBase, ToolCallParameterFields { - status: ToolCallStatus.Running; - /** How the tool was confirmed for execution */ - confirmed: ToolCallConfirmationReason; - /** The confirmation option the user selected, if confirmation options were provided */ - selectedOption?: ConfirmationOption; - /** - * Partial content produced while the tool is still executing. - * - * For example, a terminal content block lets clients subscribe to live - * output before the tool completes. - */ - content?: ToolResultContent[]; -} - -/** - * Tool finished executing, waiting for client to approve the result. - * - * @category Tool Call Types - */ -export interface ToolCallPendingResultConfirmationState extends ToolCallBase, ToolCallParameterFields, ToolCallResult { - status: ToolCallStatus.PendingResultConfirmation; - /** How the tool was confirmed for execution */ - confirmed: ToolCallConfirmationReason; - /** The confirmation option the user selected, if confirmation options were provided */ - selectedOption?: ConfirmationOption; -} - -/** - * Tool completed successfully or with an error. - * - * @category Tool Call Types - */ -export interface ToolCallCompletedState extends ToolCallBase, ToolCallParameterFields, ToolCallResult { - status: ToolCallStatus.Completed; - /** How the tool was confirmed for execution */ - confirmed: ToolCallConfirmationReason; - /** The confirmation option the user selected, if confirmation options were provided */ - selectedOption?: ConfirmationOption; -} - -/** - * Tool call was cancelled before execution. - * - * @category Tool Call Types - */ -export interface ToolCallCancelledState extends ToolCallBase, ToolCallParameterFields { - status: ToolCallStatus.Cancelled; - /** Why the tool was cancelled */ - reason: ToolCallCancellationReason; - /** Optional message explaining the cancellation */ - reasonMessage?: StringOrMarkdown; - /** What the user suggested doing instead */ - userSuggestion?: Message; - /** The confirmation option the user selected, if confirmation options were provided */ - selectedOption?: ConfirmationOption; -} - -/** - * Discriminated union of all tool call lifecycle states. - * - * See the [state model guide](/guide/state-model.html#tool-call-lifecycle) - * for the full state machine diagram. - * - * @category Tool Call Types - */ -export type ToolCallState = - | ToolCallStreamingState - | ToolCallPendingConfirmationState - | ToolCallRunningState - | ToolCallPendingResultConfirmationState - | ToolCallCompletedState - | ToolCallCancelledState; - // ─── Tool Definition Types ─────────────────────────────────────────────────── /** @@ -1268,125 +402,6 @@ export interface ToolAnnotations { openWorldHint?: boolean; } -// ─── Tool Result Content ───────────────────────────────────────────────────── - -/** - * Discriminant for tool result content types. - * - * @category Tool Result Content - */ -export const enum ToolResultContentType { - Text = 'text', - EmbeddedResource = 'embeddedResource', - Resource = 'resource', - FileEdit = 'fileEdit', - Terminal = 'terminal', - Subagent = 'subagent', -} - -/** - * Text content in a tool result. - * - * Mirrors MCP `TextContent`. - * - * @category Tool Result Content - */ -export interface ToolResultTextContent { - type: ToolResultContentType.Text; - /** The text content */ - text: string; -} - -/** - * Base64-encoded binary content embedded in a tool result. - * - * Mirrors MCP `EmbeddedResource` for inline binary data. - * - * @category Tool Result Content - */ -export interface ToolResultEmbeddedResourceContent { - type: ToolResultContentType.EmbeddedResource; - /** Base64-encoded data */ - data: string; - /** Content type (e.g. `"image/png"`, `"application/pdf"`) */ - contentType: string; -} - -/** - * A reference to a resource stored outside the tool result. - * - * Wraps {@link ContentRef} for lazy-loading large results. - * - * @category Tool Result Content - */ -export interface ToolResultResourceContent extends ContentRef { - type: ToolResultContentType.Resource; -} - -/** - * Describes a file modification performed by a tool. - * - * @category Tool Result Content - */ -export interface ToolResultFileEditContent extends FileEdit { - type: ToolResultContentType.FileEdit; -} - -/** - * A reference to a terminal whose output is relevant to this tool result. - * - * Clients can subscribe to the terminal's URI to stream its output in real - * time, providing live feedback while a tool is executing. - * - * @category Tool Result Content - */ -export interface ToolResultTerminalContent { - type: ToolResultContentType.Terminal; - /** Terminal URI (subscribable for full terminal state) */ - resource: URI; - /** Display title for the terminal content */ - title: string; -} - -/** - * A reference to a subagent session spawned by a tool. - * - * Clients can subscribe to the subagent's session URI to stream its - * progress in real time, including inner tool calls and responses. - * - * @category Tool Result Content - */ -export interface ToolResultSubagentContent { - type: ToolResultContentType.Subagent; - /** Subagent session URI (subscribable for full session state) */ - resource: URI; - /** Display title for the subagent */ - title: string; - /** Internal agent name */ - agentName?: string; - /** Human-readable description of the subagent's task */ - description?: string; -} - -/** - * Content block in a tool result. - * - * Mirrors the content blocks in MCP `CallToolResult.content`, plus - * `ToolResultResourceContent` for lazy-loading large results, - * `ToolResultFileEditContent` for file edit diffs, - * `ToolResultTerminalContent` for live terminal output, and - * `ToolResultSubagentContent` for subagent sessions (AHP extensions). - * - * @category Tool Result Content - */ -export type ToolResultContent = - | ToolResultTextContent - | ToolResultEmbeddedResourceContent - | ToolResultResourceContent - | ToolResultFileEditContent - | ToolResultTerminalContent - | ToolResultSubagentContent; - // ─── Customization Types ───────────────────────────────────────────────────── /** diff --git a/types/commands.ts b/types/commands.ts index 03931e26..c77dd949 100644 --- a/types/commands.ts +++ b/types/commands.ts @@ -10,6 +10,7 @@ export * from './common/commands.js'; export * from './channels-root/commands.js'; export * from './channels-session/commands.js'; +export * from './channels-chat/commands.js'; export * from './channels-terminal/commands.js'; export * from './channels-changeset/commands.js'; export * from './channels-resource-watch/commands.js'; diff --git a/types/common/actions.ts b/types/common/actions.ts index 4a8ddd04..09c14f9f 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -18,39 +18,21 @@ import type { import type { SessionReadyAction, SessionCreationFailedAction, - SessionTurnStartedAction, - SessionDeltaAction, - SessionResponsePartAction, - SessionToolCallStartAction, - SessionToolCallDeltaAction, - SessionToolCallReadyAction, - SessionToolCallConfirmedAction, - SessionToolCallCompleteAction, - SessionToolCallResultConfirmedAction, - SessionToolCallContentChangedAction, - SessionTurnCompleteAction, - SessionTurnCancelledAction, - SessionErrorAction, + SessionChatAddedAction, + SessionChatRemovedAction, + SessionChatUpdatedAction, + SessionDefaultChatChangedAction, SessionTitleChangedAction, - SessionUsageAction, - SessionReasoningAction, SessionModelChangedAction, SessionAgentChangedAction, SessionServerToolsChangedAction, SessionActiveClientChangedAction, SessionActiveClientToolsChangedAction, - SessionPendingMessageSetAction, - SessionPendingMessageRemovedAction, - SessionQueuedMessagesReorderedAction, - SessionInputRequestedAction, - SessionInputAnswerChangedAction, - SessionInputCompletedAction, SessionCustomizationsChangedAction, SessionCustomizationToggledAction, SessionCustomizationUpdatedAction, SessionCustomizationRemovedAction, SessionMcpServerStateChangedAction, - SessionTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, SessionActivityChangedAction, @@ -59,6 +41,31 @@ import type { SessionMetaChangedAction, } from '../channels-session/actions.js'; +import type { + ChatTurnStartedAction, + ChatDeltaAction, + ChatResponsePartAction, + ChatToolCallStartAction, + ChatToolCallDeltaAction, + ChatToolCallReadyAction, + ChatToolCallConfirmedAction, + ChatToolCallCompleteAction, + ChatToolCallResultConfirmedAction, + ChatToolCallContentChangedAction, + ChatTurnCompleteAction, + ChatTurnCancelledAction, + ChatErrorAction, + ChatUsageAction, + ChatReasoningAction, + ChatPendingMessageSetAction, + ChatPendingMessageRemovedAction, + ChatQueuedMessagesReorderedAction, + ChatInputRequestedAction, + ChatInputAnswerChangedAction, + ChatInputCompletedAction, + ChatTruncatedAction, +} from '../channels-chat/actions.js'; + import type { ChangesetStatusChangedAction, ChangesetFileSetAction, @@ -106,39 +113,43 @@ export const enum ActionType { RootActiveSessionsChanged = 'root/activeSessionsChanged', SessionReady = 'session/ready', SessionCreationFailed = 'session/creationFailed', - SessionTurnStarted = 'session/turnStarted', - SessionDelta = 'session/delta', - SessionResponsePart = 'session/responsePart', - SessionToolCallStart = 'session/toolCallStart', - SessionToolCallDelta = 'session/toolCallDelta', - SessionToolCallReady = 'session/toolCallReady', - SessionToolCallConfirmed = 'session/toolCallConfirmed', - SessionToolCallComplete = 'session/toolCallComplete', - SessionToolCallResultConfirmed = 'session/toolCallResultConfirmed', - SessionToolCallContentChanged = 'session/toolCallContentChanged', - SessionTurnComplete = 'session/turnComplete', - SessionTurnCancelled = 'session/turnCancelled', - SessionError = 'session/error', + SessionChatAdded = 'session/chatAdded', + SessionChatRemoved = 'session/chatRemoved', + SessionChatUpdated = 'session/chatUpdated', + SessionDefaultChatChanged = 'session/defaultChatChanged', + ChatTurnStarted = 'chat/turnStarted', + ChatDelta = 'chat/delta', + ChatResponsePart = 'chat/responsePart', + ChatToolCallStart = 'chat/toolCallStart', + ChatToolCallDelta = 'chat/toolCallDelta', + ChatToolCallReady = 'chat/toolCallReady', + ChatToolCallConfirmed = 'chat/toolCallConfirmed', + ChatToolCallComplete = 'chat/toolCallComplete', + ChatToolCallResultConfirmed = 'chat/toolCallResultConfirmed', + ChatToolCallContentChanged = 'chat/toolCallContentChanged', + ChatTurnComplete = 'chat/turnComplete', + ChatTurnCancelled = 'chat/turnCancelled', + ChatError = 'chat/error', SessionTitleChanged = 'session/titleChanged', - SessionUsage = 'session/usage', - SessionReasoning = 'session/reasoning', + ChatUsage = 'chat/usage', + ChatReasoning = 'chat/reasoning', SessionModelChanged = 'session/modelChanged', SessionAgentChanged = 'session/agentChanged', SessionServerToolsChanged = 'session/serverToolsChanged', SessionActiveClientChanged = 'session/activeClientChanged', SessionActiveClientToolsChanged = 'session/activeClientToolsChanged', - SessionPendingMessageSet = 'session/pendingMessageSet', - SessionPendingMessageRemoved = 'session/pendingMessageRemoved', - SessionQueuedMessagesReordered = 'session/queuedMessagesReordered', - SessionInputRequested = 'session/inputRequested', - SessionInputAnswerChanged = 'session/inputAnswerChanged', - SessionInputCompleted = 'session/inputCompleted', + ChatPendingMessageSet = 'chat/pendingMessageSet', + ChatPendingMessageRemoved = 'chat/pendingMessageRemoved', + ChatQueuedMessagesReordered = 'chat/queuedMessagesReordered', + ChatInputRequested = 'chat/inputRequested', + ChatInputAnswerChanged = 'chat/inputAnswerChanged', + ChatInputCompleted = 'chat/inputCompleted', SessionCustomizationsChanged = 'session/customizationsChanged', SessionCustomizationToggled = 'session/customizationToggled', SessionCustomizationUpdated = 'session/customizationUpdated', SessionCustomizationRemoved = 'session/customizationRemoved', SessionMcpServerStateChanged = 'session/mcpServerStateChanged', - SessionTruncated = 'session/truncated', + ChatTruncated = 'chat/truncated', SessionIsReadChanged = 'session/isReadChanged', SessionIsArchivedChanged = 'session/isArchivedChanged', SessionActivityChanged = 'session/activityChanged', @@ -212,45 +223,49 @@ export type StateAction = | RootConfigChangedAction | SessionReadyAction | SessionCreationFailedAction - | SessionTurnStartedAction - | SessionDeltaAction - | SessionResponsePartAction - | SessionToolCallStartAction - | SessionToolCallDeltaAction - | SessionToolCallReadyAction - | SessionToolCallConfirmedAction - | SessionToolCallCompleteAction - | SessionToolCallResultConfirmedAction - | SessionToolCallContentChangedAction - | SessionTurnCompleteAction - | SessionTurnCancelledAction - | SessionErrorAction + | SessionChatAddedAction + | SessionChatRemovedAction + | SessionChatUpdatedAction + | SessionDefaultChatChangedAction | SessionTitleChangedAction - | SessionUsageAction - | SessionReasoningAction | SessionModelChangedAction | SessionAgentChangedAction | SessionServerToolsChangedAction | SessionActiveClientChangedAction | SessionActiveClientToolsChangedAction - | SessionPendingMessageSetAction - | SessionPendingMessageRemovedAction - | SessionQueuedMessagesReorderedAction - | SessionInputRequestedAction - | SessionInputAnswerChangedAction - | SessionInputCompletedAction | SessionCustomizationsChangedAction | SessionCustomizationToggledAction | SessionCustomizationUpdatedAction | SessionCustomizationRemovedAction | SessionMcpServerStateChangedAction - | SessionTruncatedAction | SessionIsReadChangedAction | SessionIsArchivedChangedAction | SessionActivityChangedAction | SessionChangesetsChangedAction | SessionConfigChangedAction | SessionMetaChangedAction + | ChatTurnStartedAction + | ChatDeltaAction + | ChatResponsePartAction + | ChatToolCallStartAction + | ChatToolCallDeltaAction + | ChatToolCallReadyAction + | ChatToolCallConfirmedAction + | ChatToolCallCompleteAction + | ChatToolCallResultConfirmedAction + | ChatToolCallContentChangedAction + | ChatTurnCompleteAction + | ChatTurnCancelledAction + | ChatErrorAction + | ChatUsageAction + | ChatReasoningAction + | ChatPendingMessageSetAction + | ChatPendingMessageRemovedAction + | ChatQueuedMessagesReorderedAction + | ChatInputRequestedAction + | ChatInputAnswerChangedAction + | ChatInputCompletedAction + | ChatTruncatedAction | ChangesetStatusChangedAction | ChangesetFileSetAction | ChangesetFileRemovedAction diff --git a/types/common/messages.ts b/types/common/messages.ts index f8d8efa4..fe422f37 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -54,6 +54,10 @@ import type { CompletionsParams, CompletionsResult, } from '../channels-session/commands.js'; +import type { + CreateChatParams, + DisposeChatParams, +} from '../channels-chat/commands.js'; import type { CreateTerminalParams, DisposeTerminalParams, @@ -148,6 +152,8 @@ export interface CommandMap { 'subscribe': { params: SubscribeParams; result: SubscribeResult }; 'createSession': { params: CreateSessionParams; result: null }; 'disposeSession': { params: DisposeSessionParams; result: null }; + 'createChat': { params: CreateChatParams; result: null }; + 'disposeChat': { params: DisposeChatParams; result: null }; 'createTerminal': { params: CreateTerminalParams; result: null }; 'disposeTerminal': { params: DisposeTerminalParams; result: null }; 'createResourceWatch': { params: CreateResourceWatchParams; result: CreateResourceWatchResult }; diff --git a/types/common/state.ts b/types/common/state.ts index fd22f7f9..8c232693 100644 --- a/types/common/state.ts +++ b/types/common/state.ts @@ -16,7 +16,7 @@ import type { AnnotationsState } from '../channels-annotations/state.js'; // ─── Type Aliases ──────────────────────────────────────────────────────────── -/** A URI string (e.g. `ahp-root://` or `ahp-session:/`). */ +/** A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`). */ export type URI = string; /** @@ -322,7 +322,7 @@ export interface ErrorInfo { * @category Common Types */ export interface Snapshot { - /** The subscribed channel URI (e.g. `ahp-root://` or `ahp-session:/`) */ + /** The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`) */ resource: URI; /** The current state of the resource */ state: RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState; diff --git a/types/index.ts b/types/index.ts index 5a250428..a2459162 100644 --- a/types/index.ts +++ b/types/index.ts @@ -21,6 +21,9 @@ export type { AgentSelection, SessionState, SessionSummary, + ChatState, + ChatSummary, + ChatOrigin, ChangesSummary, SessionConfigState, Turn, @@ -62,23 +65,23 @@ export type { ToolResultSubagentContent, SessionActiveClient, PendingMessage, - SessionInputAnswer, - SessionInputAnswerValue, - SessionInputTextAnswerValue, - SessionInputNumberAnswerValue, - SessionInputBooleanAnswerValue, - SessionInputSelectedAnswerValue, - SessionInputSelectedManyAnswerValue, - SessionInputAnswered, - SessionInputSkipped, - SessionInputOption, - SessionInputQuestion, - SessionInputTextQuestion, - SessionInputNumberQuestion, - SessionInputBooleanQuestion, - SessionInputSingleSelectQuestion, - SessionInputMultiSelectQuestion, - SessionInputRequest, + ChatInputAnswer, + ChatInputAnswerValue, + ChatInputTextAnswerValue, + ChatInputNumberAnswerValue, + ChatInputBooleanAnswerValue, + ChatInputSelectedAnswerValue, + ChatInputSelectedManyAnswerValue, + ChatInputAnswered, + ChatInputSkipped, + ChatInputOption, + ChatInputQuestion, + ChatInputTextQuestion, + ChatInputNumberQuestion, + ChatInputBooleanQuestion, + ChatInputSingleSelectQuestion, + ChatInputMultiSelectQuestion, + ChatInputRequest, UsageInfo, ErrorInfo, Snapshot, @@ -107,7 +110,9 @@ export { PolicyState, SessionLifecycle, SessionStatus, + ChatOriginKind, TurnState, + MessageKind, MessageAttachmentKind, ResponsePartKind, ToolCallStatus, @@ -116,10 +121,10 @@ export { ConfirmationOptionKind, ToolResultContentType, PendingMessageKind, - SessionInputAnswerState, - SessionInputAnswerValueKind, - SessionInputQuestionKind, - SessionInputResponseKind, + ChatInputAnswerState, + ChatInputAnswerValueKind, + ChatInputQuestionKind, + ChatInputResponseKind, TerminalClaimKind, ChangesetStatus, ChangesetOperationStatus, @@ -135,36 +140,40 @@ export type { RootActiveSessionsChangedAction, SessionReadyAction, SessionCreationFailedAction, - SessionTurnStartedAction, - SessionDeltaAction, - SessionResponsePartAction, - SessionToolCallStartAction, - SessionToolCallDeltaAction, - SessionToolCallReadyAction, - SessionToolCallApprovedAction, - SessionToolCallDeniedAction, - SessionToolCallConfirmedAction, - SessionToolCallCompleteAction, - SessionToolCallResultConfirmedAction, - SessionToolCallContentChangedAction, - SessionTurnCompleteAction, - SessionTurnCancelledAction, - SessionErrorAction, + SessionChatAddedAction, + SessionChatRemovedAction, + SessionChatUpdatedAction, + SessionDefaultChatChangedAction, + ChatTurnStartedAction, + ChatDeltaAction, + ChatResponsePartAction, + ChatToolCallStartAction, + ChatToolCallDeltaAction, + ChatToolCallReadyAction, + ChatToolCallApprovedAction, + ChatToolCallDeniedAction, + ChatToolCallConfirmedAction, + ChatToolCallCompleteAction, + ChatToolCallResultConfirmedAction, + ChatToolCallContentChangedAction, + ChatTurnCompleteAction, + ChatTurnCancelledAction, + ChatErrorAction, SessionTitleChangedAction, - SessionUsageAction, - SessionReasoningAction, + ChatUsageAction, + ChatReasoningAction, SessionModelChangedAction, SessionAgentChangedAction, SessionServerToolsChangedAction, SessionActiveClientChangedAction, SessionActiveClientToolsChangedAction, - SessionPendingMessageSetAction, - SessionPendingMessageRemovedAction, - SessionQueuedMessagesReorderedAction, - SessionInputAnswerChangedAction, - SessionInputCompletedAction, - SessionInputRequestedAction, - SessionTruncatedAction, + ChatPendingMessageSetAction, + ChatPendingMessageRemovedAction, + ChatQueuedMessagesReorderedAction, + ChatInputAnswerChangedAction, + ChatInputCompletedAction, + ChatInputRequestedAction, + ChatTruncatedAction, SessionIsReadChangedAction, SessionIsArchivedChangedAction, SessionActivityChangedAction, @@ -204,6 +213,9 @@ export type { SessionAction, ClientSessionAction, ServerSessionAction, + ChatAction, + ClientChatAction, + ServerChatAction, TerminalAction, ClientTerminalAction, ServerTerminalAction, @@ -224,6 +236,7 @@ export { IS_CLIENT_DISPATCHABLE } from './action-origin.generated.js'; export { rootReducer, sessionReducer, + chatReducer, terminalReducer, changesetReducer, annotationsReducer, @@ -246,6 +259,9 @@ export type { CreateSessionParams, SessionForkSource, DisposeSessionParams, + CreateChatParams, + ChatForkSource, + DisposeChatParams, CreateTerminalParams, DisposeTerminalParams, ListSessionsParams, diff --git a/types/messages.test.ts b/types/messages.test.ts index 264a1ecb..591a9d7c 100644 --- a/types/messages.test.ts +++ b/types/messages.test.ts @@ -28,6 +28,7 @@ function readChannelSources(baseName: string): string { 'common', 'channels-root', 'channels-session', + 'channels-chat', 'channels-terminal', 'channels-changeset', 'channels-annotations', diff --git a/types/reducers.test.ts b/types/reducers.test.ts index 4a99a3dd..f3a8c7de 100644 --- a/types/reducers.test.ts +++ b/types/reducers.test.ts @@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url'; import { rootReducer, sessionReducer, + chatReducer, terminalReducer, changesetReducer, annotationsReducer, @@ -28,9 +29,8 @@ import { } from './reducers.js'; import { IS_CLIENT_DISPATCHABLE } from './action-origin.generated.js'; import { ActionType } from './actions.js'; -import type { RootState, SessionState, TerminalState, ChangesetState, AnnotationsState, ResourceWatchState } from './state.js'; +import type { RootState, SessionState, ChatState, TerminalState, ChangesetState, AnnotationsState, ResourceWatchState } from './state.js'; import { - SessionLifecycle, SessionStatus, TurnState, MessageKind, @@ -49,6 +49,7 @@ function readChannelSources(baseName: string): string { 'common', 'channels-root', 'channels-session', + 'channels-chat', 'channels-terminal', 'channels-changeset', 'channels-annotations', @@ -68,11 +69,11 @@ function readChannelSources(baseName: string): string { // ─── Fixture Loading ───────────────────────────────────────────────────────── -type FixtureState = RootState | SessionState | TerminalState | ChangesetState | AnnotationsState | ResourceWatchState; +type FixtureState = RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState | ResourceWatchState; interface Fixture { description: string; - reducer: 'root' | 'session' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch'; + reducer: 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch'; initial: FixtureState; actions: unknown[]; expected: FixtureState; @@ -129,6 +130,8 @@ describe('reducer fixtures', () => { for (const action of fixture.actions) { if (fixture.reducer === 'root') { state = rootReducer(state as RootState, action as any); + } else if (fixture.reducer === 'chat') { + state = chatReducer(state as ChatState, action as any); } else if (fixture.reducer === 'terminal') { state = terminalReducer(state as TerminalState, action as any); } else if (fixture.reducer === 'changeset') { @@ -208,7 +211,7 @@ describe('IS_CLIENT_DISPATCHABLE', () => { describe('isClientDispatchable', () => { it('returns true for client-dispatchable actions', () => { - const action = { type: ActionType.SessionTurnStarted, turnId: 't', message: { text: 'Hello', origin: { kind: MessageKind.User } } } as const; + const action = { type: ActionType.ChatTurnStarted, turnId: 't', message: { text: 'Hello', origin: { kind: MessageKind.User } } } as const; assert.equal(isClientDispatchable(action), true); }); @@ -231,17 +234,16 @@ describe('reducer immutability', () => { assert.deepStrictEqual(state.agents, []); }); - it('sessionReducer does not mutate original turns array', () => { + it('chatReducer does not mutate original turns array', () => { const turn1 = { id: 't1', message: { text: 'First', origin: { kind: MessageKind.User } }, responseParts: [], usage: undefined, state: TurnState.Complete }; const turn2 = { id: 't2', message: { text: 'Second', origin: { kind: MessageKind.User } }, responseParts: [], usage: undefined, state: TurnState.Complete }; const turn3 = { id: 't3', message: { text: 'Third', origin: { kind: MessageKind.User } }, responseParts: [], usage: undefined, state: TurnState.Complete }; - const state: SessionState = { - summary: { resource: 'x', provider: 'copilot', title: 'T', status: SessionStatus.Idle, createdAt: 1000, modifiedAt: 1000, project: { uri: 'file:///test-project', displayName: 'Test Project' } }, - lifecycle: SessionLifecycle.Ready, + const state: ChatState = { + summary: { resource: 'x', title: 'T', status: SessionStatus.Idle, modifiedAt: 1000 }, turns: [turn1, turn2, turn3], }; const original = [...state.turns]; - sessionReducer(state, { type: ActionType.SessionTruncated, turnId: 't1' }); + chatReducer(state, { type: ActionType.ChatTruncated, turnId: 't1' }); assert.deepStrictEqual(state.turns, original); }); }); diff --git a/types/reducers.ts b/types/reducers.ts index eb2d615a..d916ba3a 100644 --- a/types/reducers.ts +++ b/types/reducers.ts @@ -7,6 +7,7 @@ export { rootReducer } from './channels-root/reducer.js'; export { sessionReducer } from './channels-session/reducer.js'; +export { chatReducer } from './channels-chat/reducer.js'; export { terminalReducer } from './channels-terminal/reducer.js'; export { changesetReducer } from './channels-changeset/reducer.js'; export { annotationsReducer } from './channels-annotations/reducer.js'; diff --git a/types/state.ts b/types/state.ts index 03de20b7..8748e48d 100644 --- a/types/state.ts +++ b/types/state.ts @@ -10,6 +10,7 @@ export * from './common/state.js'; export * from './channels-root/state.js'; export * from './channels-session/state.js'; +export * from './channels-chat/state.js'; export * from './channels-terminal/state.js'; export * from './channels-changeset/state.js'; export * from './channels-annotations/state.js'; diff --git a/types/test-cases/reducers/003-session-ready.json b/types/test-cases/reducers/003-session-ready.json index 8f17b7fb..575f9c79 100644 --- a/types/test-cases/reducers/003-session-ready.json +++ b/types/test-cases/reducers/003-session-ready.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -28,6 +28,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/004-session-creationfailed.json b/types/test-cases/reducers/004-session-creationfailed.json index 24564838..aa25cdd6 100644 --- a/types/test-cases/reducers/004-session-creationfailed.json +++ b/types/test-cases/reducers/004-session-creationfailed.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -32,10 +32,10 @@ "modifiedAt": 1000 }, "lifecycle": "creationFailed", - "turns": [], "creationError": { "errorType": "init", "message": "Failed to start" - } + }, + "chats": [] } } diff --git a/types/test-cases/reducers/005-session-turnstarted.json b/types/test-cases/reducers/005-session-turnstarted.json index 30bcdd89..3c3576ad 100644 --- a/types/test-cases/reducers/005-session-turnstarted.json +++ b/types/test-cases/reducers/005-session-turnstarted.json @@ -1,21 +1,16 @@ { "description": "session/turnStarted", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "turn-1", "message": { "text": "Hello", @@ -26,15 +21,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -46,6 +32,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json b/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json index ffd5c5fc..8636d1b2 100644 --- a/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json +++ b/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json @@ -1,16 +1,7 @@ { "description": "turnStarted with queuedMessageId removes from queuedMessages", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [], "queuedMessages": [ { @@ -31,11 +22,15 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "turn-1", "message": { "text": "First", @@ -47,16 +42,18 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], + "activeTurn": { + "id": "turn-1", + "message": { + "text": "First", + "origin": { + "kind": "user" + } + }, + "responseParts": [], + "usage": null + }, "queuedMessages": [ { "id": "q-2", @@ -68,16 +65,9 @@ } } ], - "activeTurn": { - "id": "turn-1", - "message": { - "text": "First", - "origin": { - "kind": "user" - } - }, - "responseParts": [], - "usage": null - } + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json b/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json index 2cdab800..15b4ecf4 100644 --- a/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json +++ b/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json @@ -1,16 +1,7 @@ { "description": "turnStarted with queuedMessageId removes last queued message", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [], "queuedMessages": [ { @@ -22,11 +13,15 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "turn-1", "message": { "text": "Only", @@ -38,17 +33,7 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], - "queuedMessages": null, "activeTurn": { "id": "turn-1", "message": { @@ -59,6 +44,11 @@ }, "responseParts": [], "usage": null - } + }, + "queuedMessages": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json b/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json index 2fba0ce0..c955affc 100644 --- a/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json +++ b/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json @@ -1,16 +1,7 @@ { "description": "turnStarted with queuedMessageId removes matching steering message", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [], "steeringMessage": { "id": "s-1", @@ -20,11 +11,15 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "turn-1", "message": { "text": "Steer", @@ -36,17 +31,7 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], - "steeringMessage": null, "activeTurn": { "id": "turn-1", "message": { @@ -57,6 +42,11 @@ }, "responseParts": [], "usage": null - } + }, + "steeringMessage": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json b/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json index 1f5da132..4b1418e5 100644 --- a/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json +++ b/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json @@ -1,16 +1,7 @@ { "description": "turnStarted without queuedMessageId does not touch pending messages", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [], "steeringMessage": { "id": "s-1", @@ -31,11 +22,15 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "turn-1", "message": { "text": "Hello", @@ -46,16 +41,18 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], + "activeTurn": { + "id": "turn-1", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [], + "usage": null + }, "steeringMessage": { "id": "s-1", "message": { @@ -76,16 +73,9 @@ } } ], - "activeTurn": { - "id": "turn-1", - "message": { - "text": "Hello", - "origin": { - "kind": "user" - } - }, - "responseParts": [], - "usage": null - } + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/010-session-delta-appends-content.json b/types/test-cases/reducers/010-session-delta-appends-content.json index 12de3271..f4cefe7e 100644 --- a/types/test-cases/reducers/010-session-delta-appends-content.json +++ b/types/test-cases/reducers/010-session-delta-appends-content.json @@ -1,16 +1,7 @@ { "description": "session/delta appends content", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "turn-1", "part": { "kind": "markdown", @@ -35,28 +30,19 @@ } }, { - "type": "session/delta", + "type": "chat/delta", "turnId": "turn-1", "partId": "md-1", "content": "Hello " }, { - "type": "session/delta", + "type": "chat/delta", "turnId": "turn-1", "partId": "md-1", "content": "world" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -74,6 +60,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json b/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json index e0152300..c7fef529 100644 --- a/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "session/delta with wrong turnId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -28,26 +19,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/delta", + "type": "chat/delta", "turnId": "wrong-turn", "partId": "md-1", "content": "orphan" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -65,6 +51,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/012-session-delta-without-activeturn-is-no-op.json b/types/test-cases/reducers/012-session-delta-without-activeturn-is-no-op.json index 6f6b1f60..604b4a55 100644 --- a/types/test-cases/reducers/012-session-delta-without-activeturn-is-no-op.json +++ b/types/test-cases/reducers/012-session-delta-without-activeturn-is-no-op.json @@ -1,36 +1,26 @@ { "description": "session/delta without activeTurn is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/delta", + "type": "chat/delta", "turnId": "turn-1", "partId": "md-1", "content": "orphan" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json b/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json index 3708ca8b..5ea45c46 100644 --- a/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json +++ b/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json @@ -1,16 +1,7 @@ { "description": "session/responsePart adds to responseParts", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "turn-1", "part": { "kind": "markdown", @@ -36,15 +31,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -62,6 +48,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json b/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json index 382753b3..2e4f14b9 100644 --- a/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json +++ b/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json @@ -1,16 +1,7 @@ { "description": "session/turnComplete finalizes turn", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "turn-1", "part": { "kind": "markdown", @@ -35,26 +30,17 @@ } }, { - "type": "session/delta", + "type": "chat/delta", "turnId": "turn-1", "partId": "md-1", "content": "Response text" }, { - "type": "session/turnComplete", + "type": "chat/turnComplete", "turnId": "turn-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "turn-1", @@ -76,6 +62,10 @@ "error": null } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json b/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json index c53aac51..61677b59 100644 --- a/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json +++ b/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json @@ -1,16 +1,7 @@ { "description": "session/turnCancelled finalizes turn", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,24 +13,19 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/turnCancelled", + "type": "chat/turnCancelled", "turnId": "turn-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "turn-1", @@ -55,6 +41,10 @@ "error": null } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json b/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json index 232db4a8..40c41bbd 100644 --- a/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json +++ b/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json @@ -1,16 +1,7 @@ { "description": "session/error finalizes turn with error", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/error", + "type": "chat/error", "turnId": "turn-1", "error": { "errorType": "runtime", @@ -35,15 +30,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 2, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "turn-1", @@ -62,6 +48,10 @@ } } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 2, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json b/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json index 17e0a28f..7e13a9b1 100644 --- a/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json +++ b/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json @@ -1,16 +1,7 @@ { "description": "turnComplete force-cancels in-progress tool calls", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,31 +13,26 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/turnComplete", + "type": "chat/turnComplete", "turnId": "turn-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "turn-1", @@ -77,6 +63,10 @@ "error": null } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json b/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json index 8f3335ec..42df2388 100644 --- a/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "turnComplete with wrong turnId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,24 +13,19 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/turnComplete", + "type": "chat/turnComplete", "turnId": "wrong-turn" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -51,6 +37,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json index d4ac1ec5..aacc8adf 100644 --- a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json +++ b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json @@ -1,16 +1,7 @@ { "description": "tool call full lifecycle: start → delta → ready → confirmed → complete", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,39 +13,43 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallDelta", + "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-1", "content": "ls -la", "invocationMessage": "Listing files" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: ls -la", "toolInput": "ls -la" }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, "confirmed": "user-action" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -64,15 +59,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -101,6 +87,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json b/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json index c61b0d5d..998896e5 100644 --- a/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json +++ b/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json @@ -1,16 +1,7 @@ { "description": "tool call ready with auto-confirm transitions to running", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run", @@ -41,15 +36,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -76,6 +62,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json b/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json index d76222d7..da4d8c5f 100644 --- a/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json +++ b/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json @@ -1,16 +1,7 @@ { "description": "tool call denied transitions to cancelled", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,24 +13,28 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: rm -rf /" }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": false, @@ -47,15 +42,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -84,6 +70,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json b/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json index 238d17dc..41a81c1c 100644 --- a/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json +++ b/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json @@ -1,16 +1,7 @@ { "description": "tool call result confirmation → pending → approved", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,25 +13,29 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run", "confirmed": "not-needed" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -50,22 +45,13 @@ "requiresResultConfirmation": true }, { - "type": "session/toolCallResultConfirmed", + "type": "chat/toolCallResultConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -97,6 +83,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json b/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json index 3629807a..4da3beee 100644 --- a/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json +++ b/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json @@ -1,16 +1,7 @@ { "description": "tool call result denied → cancelled with result-denied reason", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,25 +13,29 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run", "confirmed": "not-needed" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -50,22 +45,13 @@ "requiresResultConfirmation": true }, { - "type": "session/toolCallResultConfirmed", + "type": "chat/toolCallResultConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": false } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -92,6 +78,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json b/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json index df5d0904..209bfdca 100644 --- a/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json +++ b/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json @@ -1,16 +1,7 @@ { "description": "tool call complete from pending-confirmation defaults confirmed", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,24 +13,28 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -49,15 +44,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -86,6 +72,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json b/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json index 8f130d70..b4c0d1d9 100644 --- a/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json +++ b/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json @@ -1,16 +1,7 @@ { "description": "tool call actions for unknown toolCallId are no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,26 +13,21 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallDelta", + "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "nonexistent", "content": "data" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -53,6 +39,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json index 90433f29..7f726896 100644 --- a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json +++ b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json @@ -1,16 +1,7 @@ { "description": "toolCallReady transitions running tool back to pending-confirmation", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,25 +13,29 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run", "confirmed": "not-needed" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: rm -rf /tmp/test", @@ -51,15 +46,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -91,6 +77,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json b/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json index e0628e8a..2c73cba5 100644 --- a/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json +++ b/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json @@ -1,16 +1,7 @@ { "description": "toolCallReady re-confirmation approved transitions back to running", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,31 +13,35 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run", "confirmed": "not-needed" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Permission needed" }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, @@ -54,15 +49,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -89,6 +75,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json b/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json index 48d208dd..df31e6b1 100644 --- a/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json +++ b/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json @@ -1,16 +1,7 @@ { "description": "toolCallReady re-confirmation denied transitions to cancelled", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,31 +13,35 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run", "confirmed": "not-needed" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Permission needed" }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": false, @@ -54,15 +49,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -91,6 +77,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json b/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json index 4f72e4f7..c4d068f5 100644 --- a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json +++ b/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json @@ -1,16 +1,7 @@ { "description": "toolCallReady ignores non-streaming/non-running tool calls", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -37,26 +28,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run again" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -83,6 +69,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/030-session-titlechanged-updates-title.json b/types/test-cases/reducers/030-session-titlechanged-updates-title.json index 448d6306..9a8af04a 100644 --- a/types/test-cases/reducers/030-session-titlechanged-updates-title.json +++ b/types/test-cases/reducers/030-session-titlechanged-updates-title.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -29,6 +29,6 @@ "modifiedAt": 9999 }, "lifecycle": "creating", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json b/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json index ef851304..c9cd4ec4 100644 --- a/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json +++ b/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json @@ -1,16 +1,7 @@ { "description": "session/usage updates usage on active turn", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/usage", + "type": "chat/usage", "turnId": "turn-1", "usage": { "inputTokens": 100, @@ -35,15 +30,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -58,6 +44,10 @@ "inputTokens": 100, "outputTokens": 50 } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json b/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json index cd88f26d..40338a9c 100644 --- a/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json +++ b/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json @@ -1,16 +1,7 @@ { "description": "session/reasoning appends reasoning content", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "turn-1", "part": { "kind": "reasoning", @@ -35,28 +30,19 @@ } }, { - "type": "session/reasoning", + "type": "chat/reasoning", "turnId": "turn-1", "partId": "r-1", "content": "Thinking about " }, { - "type": "session/reasoning", + "type": "chat/reasoning", "turnId": "turn-1", "partId": "r-1", "content": "the answer" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -74,6 +60,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/033-session-modelchanged-updates-model.json b/types/test-cases/reducers/033-session-modelchanged-updates-model.json index 650534b8..61bf7a6e 100644 --- a/types/test-cases/reducers/033-session-modelchanged-updates-model.json +++ b/types/test-cases/reducers/033-session-modelchanged-updates-model.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -34,6 +34,6 @@ } }, "lifecycle": "creating", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/034-session-servertoolschanged-sets-server-tools.json b/types/test-cases/reducers/034-session-servertoolschanged-sets-server-tools.json index b76d9be0..67919eed 100644 --- a/types/test-cases/reducers/034-session-servertoolschanged-sets-server-tools.json +++ b/types/test-cases/reducers/034-session-servertoolschanged-sets-server-tools.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -34,12 +34,12 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "serverTools": [ { "name": "bash", "description": "Run shell commands" } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/035-session-activeclientchanged-sets-client.json b/types/test-cases/reducers/035-session-activeclientchanged-sets-client.json index fa8b4b9d..640d02a3 100644 --- a/types/test-cases/reducers/035-session-activeclientchanged-sets-client.json +++ b/types/test-cases/reducers/035-session-activeclientchanged-sets-client.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -33,11 +33,11 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "activeClient": { "clientId": "vscode-1", "displayName": "VS Code", "tools": [] - } + }, + "chats": [] } } diff --git a/types/test-cases/reducers/036-session-activeclientchanged-unsets-client.json b/types/test-cases/reducers/036-session-activeclientchanged-unsets-client.json index 8322a081..9d330118 100644 --- a/types/test-cases/reducers/036-session-activeclientchanged-unsets-client.json +++ b/types/test-cases/reducers/036-session-activeclientchanged-unsets-client.json @@ -11,11 +11,11 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "activeClient": { "clientId": "vscode-1", "tools": [] - } + }, + "chats": [] }, "actions": [ { @@ -33,7 +33,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], - "activeClient": null + "activeClient": null, + "chats": [] } } diff --git a/types/test-cases/reducers/037-session-activeclienttoolschanged-updates-tools.json b/types/test-cases/reducers/037-session-activeclienttoolschanged-updates-tools.json index e8542617..c895cd60 100644 --- a/types/test-cases/reducers/037-session-activeclienttoolschanged-updates-tools.json +++ b/types/test-cases/reducers/037-session-activeclienttoolschanged-updates-tools.json @@ -11,11 +11,11 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "activeClient": { "clientId": "vscode-1", "tools": [] - } + }, + "chats": [] }, "actions": [ { @@ -38,7 +38,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "activeClient": { "clientId": "vscode-1", "tools": [ @@ -47,6 +46,7 @@ "description": "Open a file" } ] - } + }, + "chats": [] } } diff --git a/types/test-cases/reducers/038-session-activeclienttoolschanged-without-activeclient-is-no-op.json b/types/test-cases/reducers/038-session-activeclienttoolschanged-without-activeclient-is-no-op.json index 7cd707c4..8153b47e 100644 --- a/types/test-cases/reducers/038-session-activeclienttoolschanged-without-activeclient-is-no-op.json +++ b/types/test-cases/reducers/038-session-activeclienttoolschanged-without-activeclient-is-no-op.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -33,6 +33,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/039-set-steering-message.json b/types/test-cases/reducers/039-set-steering-message.json index 5c588131..a5a1a1ef 100644 --- a/types/test-cases/reducers/039-set-steering-message.json +++ b/types/test-cases/reducers/039-set-steering-message.json @@ -1,21 +1,16 @@ { "description": "set steering message", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "steering", "id": "sm-1", "message": { @@ -27,15 +22,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-1", @@ -45,6 +31,10 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/040-replace-existing-steering-message.json b/types/test-cases/reducers/040-replace-existing-steering-message.json index 44066d54..380fadc2 100644 --- a/types/test-cases/reducers/040-replace-existing-steering-message.json +++ b/types/test-cases/reducers/040-replace-existing-steering-message.json @@ -1,16 +1,7 @@ { "description": "replace existing steering message", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-1", @@ -20,11 +11,15 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "steering", "id": "sm-2", "message": { @@ -36,15 +31,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-2", @@ -54,6 +40,10 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/041-update-steering-message-content-via-set-with-same-id.json b/types/test-cases/reducers/041-update-steering-message-content-via-set-with-same-id.json index c0651dfb..a101a837 100644 --- a/types/test-cases/reducers/041-update-steering-message-content-via-set-with-same-id.json +++ b/types/test-cases/reducers/041-update-steering-message-content-via-set-with-same-id.json @@ -1,16 +1,7 @@ { "description": "update steering message content via set with same ID", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-1", @@ -20,11 +11,15 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "steering", "id": "sm-1", "message": { @@ -36,15 +31,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-1", @@ -54,6 +40,10 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/042-remove-steering-message.json b/types/test-cases/reducers/042-remove-steering-message.json index 12f22c62..7c4700e3 100644 --- a/types/test-cases/reducers/042-remove-steering-message.json +++ b/types/test-cases/reducers/042-remove-steering-message.json @@ -1,16 +1,7 @@ { "description": "remove steering message", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-1", @@ -20,26 +11,25 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageRemoved", + "type": "chat/pendingMessageRemoved", "kind": "steering", "id": "sm-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], - "steeringMessage": null + "steeringMessage": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/043-remove-steering-message-is-no-op-for-mismatched-id.json b/types/test-cases/reducers/043-remove-steering-message-is-no-op-for-mismatched-id.json index fdf9e741..1e8ae5ca 100644 --- a/types/test-cases/reducers/043-remove-steering-message-is-no-op-for-mismatched-id.json +++ b/types/test-cases/reducers/043-remove-steering-message-is-no-op-for-mismatched-id.json @@ -1,16 +1,7 @@ { "description": "remove steering message is no-op for mismatched ID", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-1", @@ -20,25 +11,20 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageRemoved", + "type": "chat/pendingMessageRemoved", "kind": "steering", "id": "sm-unknown" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "sm-1", @@ -48,6 +34,10 @@ "kind": "user" } } - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/044-remove-steering-message-is-no-op-when-none-exists.json b/types/test-cases/reducers/044-remove-steering-message-is-no-op-when-none-exists.json index 078caf1b..105b63ce 100644 --- a/types/test-cases/reducers/044-remove-steering-message-is-no-op-when-none-exists.json +++ b/types/test-cases/reducers/044-remove-steering-message-is-no-op-when-none-exists.json @@ -1,35 +1,25 @@ { "description": "remove steering message is no-op when none exists", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageRemoved", + "type": "chat/pendingMessageRemoved", "kind": "steering", "id": "sm-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/045-set-a-new-queued-message.json b/types/test-cases/reducers/045-set-a-new-queued-message.json index 844ba532..27c33b98 100644 --- a/types/test-cases/reducers/045-set-a-new-queued-message.json +++ b/types/test-cases/reducers/045-set-a-new-queued-message.json @@ -1,21 +1,16 @@ { "description": "set a new queued message", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "queued", "id": "pm-1", "message": { @@ -27,15 +22,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -47,6 +33,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/046-append-queued-message-when-id-is-new.json b/types/test-cases/reducers/046-append-queued-message-when-id-is-new.json index 7a108cdc..1d1fc70b 100644 --- a/types/test-cases/reducers/046-append-queued-message-when-id-is-new.json +++ b/types/test-cases/reducers/046-append-queued-message-when-id-is-new.json @@ -1,16 +1,7 @@ { "description": "append queued message when ID is new", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -22,11 +13,15 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "queued", "id": "pm-2", "message": { @@ -38,15 +33,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -67,6 +53,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/047-update-queued-message-in-place-when-id-already-exists.json b/types/test-cases/reducers/047-update-queued-message-in-place-when-id-already-exists.json index 802d666d..d4f7aeeb 100644 --- a/types/test-cases/reducers/047-update-queued-message-in-place-when-id-already-exists.json +++ b/types/test-cases/reducers/047-update-queued-message-in-place-when-id-already-exists.json @@ -1,16 +1,7 @@ { "description": "update queued message in place when ID already exists", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -31,11 +22,15 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "queued", "id": "pm-1", "message": { @@ -47,15 +42,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -76,6 +62,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/048-remove-a-queued-message.json b/types/test-cases/reducers/048-remove-a-queued-message.json index 0f089fec..ee5aac54 100644 --- a/types/test-cases/reducers/048-remove-a-queued-message.json +++ b/types/test-cases/reducers/048-remove-a-queued-message.json @@ -1,16 +1,7 @@ { "description": "remove a queued message", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -31,25 +22,20 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageRemoved", + "type": "chat/pendingMessageRemoved", "kind": "queued", "id": "pm-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -61,6 +47,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/049-remove-last-queued-message-sets-array-to-undefined.json b/types/test-cases/reducers/049-remove-last-queued-message-sets-array-to-undefined.json index ef0ae69e..17da3465 100644 --- a/types/test-cases/reducers/049-remove-last-queued-message-sets-array-to-undefined.json +++ b/types/test-cases/reducers/049-remove-last-queued-message-sets-array-to-undefined.json @@ -1,16 +1,7 @@ { "description": "remove last queued message sets array to undefined", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -22,26 +13,25 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageRemoved", + "type": "chat/pendingMessageRemoved", "kind": "queued", "id": "pm-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], - "queuedMessages": null + "queuedMessages": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/050-remove-queued-message-is-no-op-for-unknown-id.json b/types/test-cases/reducers/050-remove-queued-message-is-no-op-for-unknown-id.json index fea82c7f..dd366545 100644 --- a/types/test-cases/reducers/050-remove-queued-message-is-no-op-for-unknown-id.json +++ b/types/test-cases/reducers/050-remove-queued-message-is-no-op-for-unknown-id.json @@ -1,16 +1,7 @@ { "description": "remove queued message is no-op for unknown ID", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -22,25 +13,20 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageRemoved", + "type": "chat/pendingMessageRemoved", "kind": "queued", "id": "pm-unknown" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -52,6 +38,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/051-remove-queued-message-is-no-op-when-array-is-empty.json b/types/test-cases/reducers/051-remove-queued-message-is-no-op-when-array-is-empty.json index 4018812b..f29a1478 100644 --- a/types/test-cases/reducers/051-remove-queued-message-is-no-op-when-array-is-empty.json +++ b/types/test-cases/reducers/051-remove-queued-message-is-no-op-when-array-is-empty.json @@ -1,35 +1,25 @@ { "description": "remove queued message is no-op when array is empty", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageRemoved", + "type": "chat/pendingMessageRemoved", "kind": "queued", "id": "pm-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/052-steering-and-queued-messages-are-independent.json b/types/test-cases/reducers/052-steering-and-queued-messages-are-independent.json index 5986b9b4..ca4fbdf7 100644 --- a/types/test-cases/reducers/052-steering-and-queued-messages-are-independent.json +++ b/types/test-cases/reducers/052-steering-and-queued-messages-are-independent.json @@ -1,21 +1,16 @@ { "description": "steering and queued messages are independent", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "steering", "id": "s-1", "message": { @@ -26,7 +21,7 @@ } }, { - "type": "session/pendingMessageSet", + "type": "chat/pendingMessageSet", "kind": "queued", "id": "q-1", "message": { @@ -38,15 +33,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "steeringMessage": { "id": "s-1", @@ -67,6 +53,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/053-reorders-queued-messages.json b/types/test-cases/reducers/053-reorders-queued-messages.json index fdfbb362..3f388de7 100644 --- a/types/test-cases/reducers/053-reorders-queued-messages.json +++ b/types/test-cases/reducers/053-reorders-queued-messages.json @@ -1,16 +1,7 @@ { "description": "reorders queued messages", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -40,11 +31,15 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/queuedMessagesReordered", + "type": "chat/queuedMessagesReordered", "order": [ "c", "a", @@ -53,15 +48,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -91,6 +77,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/054-keeps-messages-not-in-order-at-end-in-original-order.json b/types/test-cases/reducers/054-keeps-messages-not-in-order-at-end-in-original-order.json index a1bc7466..c02b328a 100644 --- a/types/test-cases/reducers/054-keeps-messages-not-in-order-at-end-in-original-order.json +++ b/types/test-cases/reducers/054-keeps-messages-not-in-order-at-end-in-original-order.json @@ -1,16 +1,7 @@ { "description": "keeps messages not in order at end in original order", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -40,26 +31,21 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/queuedMessagesReordered", + "type": "chat/queuedMessagesReordered", "order": [ "c" ] } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -89,6 +75,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/055-ignores-unknown-ids-in-reorder.json b/types/test-cases/reducers/055-ignores-unknown-ids-in-reorder.json index fdb2dca1..68f91aec 100644 --- a/types/test-cases/reducers/055-ignores-unknown-ids-in-reorder.json +++ b/types/test-cases/reducers/055-ignores-unknown-ids-in-reorder.json @@ -1,16 +1,7 @@ { "description": "ignores unknown IDs in reorder", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -31,11 +22,15 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/queuedMessagesReordered", + "type": "chat/queuedMessagesReordered", "order": [ "unknown", "b", @@ -45,15 +40,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -74,6 +60,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/056-empty-reorder-preserves-all-messages-in-original-order.json b/types/test-cases/reducers/056-empty-reorder-preserves-all-messages-in-original-order.json index 30a400ea..4530500a 100644 --- a/types/test-cases/reducers/056-empty-reorder-preserves-all-messages-in-original-order.json +++ b/types/test-cases/reducers/056-empty-reorder-preserves-all-messages-in-original-order.json @@ -1,16 +1,7 @@ { "description": "empty reorder preserves all messages in original order", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -22,24 +13,19 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/queuedMessagesReordered", + "type": "chat/queuedMessagesReordered", "order": [] } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", "turns": [], "queuedMessages": [ { @@ -51,6 +37,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/057-reorder-is-no-op-when-no-queued-messages-exist.json b/types/test-cases/reducers/057-reorder-is-no-op-when-no-queued-messages-exist.json index 17d7517d..fadc8e3a 100644 --- a/types/test-cases/reducers/057-reorder-is-no-op-when-no-queued-messages-exist.json +++ b/types/test-cases/reducers/057-reorder-is-no-op-when-no-queued-messages-exist.json @@ -1,21 +1,16 @@ { "description": "reorder is no-op when no queued messages exist", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/queuedMessagesReordered", + "type": "chat/queuedMessagesReordered", "order": [ "a", "b" @@ -23,15 +18,10 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "creating", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json b/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json index ca18fa17..77509bbb 100644 --- a/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json +++ b/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -45,7 +45,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -62,6 +61,7 @@ "enabled": false, "clientId": "client-1" } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json b/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json index 73e8fdd3..0b45b67e 100644 --- a/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json +++ b/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -20,7 +19,8 @@ "name": "Plugin A", "enabled": true } - ] + ], + "chats": [] }, "actions": [ { @@ -46,7 +46,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -55,6 +54,7 @@ "name": "Plugin B", "enabled": false } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json b/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json index 1470f328..43904b20 100644 --- a/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json +++ b/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -27,7 +26,8 @@ "name": "Plugin B", "enabled": true } - ] + ], + "chats": [] }, "actions": [ { @@ -46,7 +46,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -62,6 +61,7 @@ "name": "Plugin B", "enabled": true } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json b/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json index 8aad7c6c..3df3cc84 100644 --- a/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json +++ b/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -20,7 +19,8 @@ "name": "Plugin A", "enabled": true } - ] + ], + "chats": [] }, "actions": [ { @@ -39,7 +39,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -48,6 +47,7 @@ "name": "Plugin A", "enabled": true } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json b/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json index f5acdccb..630a0805 100644 --- a/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json +++ b/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -30,6 +30,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/063-session-truncated-keeps-turns-up-to-turnid.json b/types/test-cases/reducers/063-session-truncated-keeps-turns-up-to-turnid.json index 4bad1ba0..e744a542 100644 --- a/types/test-cases/reducers/063-session-truncated-keeps-turns-up-to-turnid.json +++ b/types/test-cases/reducers/063-session-truncated-keeps-turns-up-to-turnid.json @@ -1,16 +1,7 @@ { "description": "session/truncated keeps turns up to turnId", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -48,24 +39,19 @@ "usage": null, "state": "complete" } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/truncated", + "type": "chat/truncated", "turnId": "t2" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -92,6 +78,10 @@ "state": "complete" } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/064-session-truncated-keeps-all-when-turnid-is-last.json b/types/test-cases/reducers/064-session-truncated-keeps-all-when-turnid-is-last.json index 7e13b650..edc7a4da 100644 --- a/types/test-cases/reducers/064-session-truncated-keeps-all-when-turnid-is-last.json +++ b/types/test-cases/reducers/064-session-truncated-keeps-all-when-turnid-is-last.json @@ -1,16 +1,7 @@ { "description": "session/truncated keeps all when turnId is last", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -36,24 +27,19 @@ "usage": null, "state": "complete" } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/truncated", + "type": "chat/truncated", "turnId": "t2" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -80,6 +66,10 @@ "state": "complete" } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/065-session-truncated-keeps-only-first-when-turnid-is-first.json b/types/test-cases/reducers/065-session-truncated-keeps-only-first-when-turnid-is-first.json index 4ad6df4b..9d7bb9e1 100644 --- a/types/test-cases/reducers/065-session-truncated-keeps-only-first-when-turnid-is-first.json +++ b/types/test-cases/reducers/065-session-truncated-keeps-only-first-when-turnid-is-first.json @@ -1,16 +1,7 @@ { "description": "session/truncated keeps only first when turnId is first", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -48,24 +39,19 @@ "usage": null, "state": "complete" } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/truncated", + "type": "chat/truncated", "turnId": "t1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -80,6 +66,10 @@ "state": "complete" } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/066-session-truncated-clears-all-when-turnid-omitted.json b/types/test-cases/reducers/066-session-truncated-clears-all-when-turnid-omitted.json index 4514e893..c27c0cfc 100644 --- a/types/test-cases/reducers/066-session-truncated-clears-all-when-turnid-omitted.json +++ b/types/test-cases/reducers/066-session-truncated-clears-all-when-turnid-omitted.json @@ -1,16 +1,7 @@ { "description": "session/truncated clears all when turnId omitted", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -48,24 +39,23 @@ "usage": null, "state": "complete" } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/truncated" + "type": "chat/truncated" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/067-session-truncated-drops-active-turn.json b/types/test-cases/reducers/067-session-truncated-drops-active-turn.json index ea058dc6..d5dde94f 100644 --- a/types/test-cases/reducers/067-session-truncated-drops-active-turn.json +++ b/types/test-cases/reducers/067-session-truncated-drops-active-turn.json @@ -1,16 +1,7 @@ { "description": "session/truncated drops active turn", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -47,24 +38,19 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/truncated", + "type": "chat/truncated", "turnId": "t1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -79,6 +65,10 @@ "state": "complete" } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json b/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json index 2738add3..e9435f3c 100644 --- a/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json +++ b/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json @@ -1,16 +1,7 @@ { "description": "session/truncated drops active turn even when clearing all", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,24 +13,23 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/truncated" + "type": "chat/truncated" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/069-session-truncated-is-no-op-for-unknown-turnid.json b/types/test-cases/reducers/069-session-truncated-is-no-op-for-unknown-turnid.json index 103ec926..19a10953 100644 --- a/types/test-cases/reducers/069-session-truncated-is-no-op-for-unknown-turnid.json +++ b/types/test-cases/reducers/069-session-truncated-is-no-op-for-unknown-turnid.json @@ -1,16 +1,7 @@ { "description": "session/truncated is no-op for unknown turnId", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -36,24 +27,19 @@ "usage": null, "state": "complete" } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/truncated", + "type": "chat/truncated", "turnId": "unknown" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -79,6 +65,10 @@ "usage": null, "state": "complete" } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json index 7fd6ebde..e0420051 100644 --- a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json +++ b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json @@ -1,21 +1,16 @@ { "description": "full turn flow with tool calls and re-confirmation", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "t1", "message": { "text": "Fix the bug", @@ -25,7 +20,7 @@ } }, { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "t1", "part": { "kind": "markdown", @@ -34,33 +29,33 @@ } }, { - "type": "session/delta", + "type": "chat/delta", "turnId": "t1", "partId": "md-1", "content": "I will " }, { - "type": "session/delta", + "type": "chat/delta", "turnId": "t1", "partId": "md-1", "content": "fix it." }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "t1", "toolCallId": "tc1", "toolName": "edit", "displayName": "Edit File" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "t1", "toolCallId": "tc1", "invocationMessage": "Edit main.ts", "confirmed": "not-needed" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "t1", "toolCallId": "tc1", "result": { @@ -69,21 +64,21 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "t1", "toolCallId": "tc2", "toolName": "write", "displayName": "Write File" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "t1", "toolCallId": "tc2", "invocationMessage": "Write /tmp/out", "confirmed": "not-needed" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "t1", "toolCallId": "tc2", "invocationMessage": "Write to /tmp/out", @@ -93,14 +88,14 @@ } }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "t1", "toolCallId": "tc2", "approved": true, "confirmed": "user-action" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "t1", "toolCallId": "tc2", "result": { @@ -109,7 +104,7 @@ } }, { - "type": "session/usage", + "type": "chat/usage", "turnId": "t1", "usage": { "inputTokens": 200, @@ -117,7 +112,7 @@ } }, { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "t1", "part": { "kind": "reasoning", @@ -126,13 +121,13 @@ } }, { - "type": "session/reasoning", + "type": "chat/reasoning", "turnId": "t1", "partId": "r-1", "content": "The bug was in line 42" }, { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "t1", "part": { "kind": "markdown", @@ -141,20 +136,11 @@ } }, { - "type": "session/turnComplete", + "type": "chat/turnComplete", "turnId": "t1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "t1", @@ -224,6 +210,10 @@ "error": null } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/071-session-isreadchanged-marks-session-as-read.json b/types/test-cases/reducers/071-session-isreadchanged-marks-session-as-read.json index d8f4aaa0..7d03fe34 100644 --- a/types/test-cases/reducers/071-session-isreadchanged-marks-session-as-read.json +++ b/types/test-cases/reducers/071-session-isreadchanged-marks-session-as-read.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -29,6 +29,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/072-session-isreadchanged-marks-session-as-unread.json b/types/test-cases/reducers/072-session-isreadchanged-marks-session-as-unread.json index 3fd8fb44..94959691 100644 --- a/types/test-cases/reducers/072-session-isreadchanged-marks-session-as-unread.json +++ b/types/test-cases/reducers/072-session-isreadchanged-marks-session-as-unread.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -29,6 +29,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/073-session-isarchivedchanged-marks-session-as-archived.json b/types/test-cases/reducers/073-session-isarchivedchanged-marks-session-as-archived.json index 9b1686bc..738e724a 100644 --- a/types/test-cases/reducers/073-session-isarchivedchanged-marks-session-as-archived.json +++ b/types/test-cases/reducers/073-session-isarchivedchanged-marks-session-as-archived.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -29,6 +29,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/074-session-isarchivedchanged-unarchives-session.json b/types/test-cases/reducers/074-session-isarchivedchanged-unarchives-session.json index f068c356..299a9df6 100644 --- a/types/test-cases/reducers/074-session-isarchivedchanged-unarchives-session.json +++ b/types/test-cases/reducers/074-session-isarchivedchanged-unarchives-session.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -29,6 +29,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/075-turnstarted-clears-isread.json b/types/test-cases/reducers/075-turnstarted-clears-isread.json index 5bda987f..80005421 100644 --- a/types/test-cases/reducers/075-turnstarted-clears-isread.json +++ b/types/test-cases/reducers/075-turnstarted-clears-isread.json @@ -1,21 +1,16 @@ { "description": "session/turnStarted clears isRead on a previously-read session", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 33, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 33, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "turn-1", "message": { "text": "Hello", @@ -26,15 +21,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -46,6 +32,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json b/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json index 43ffb35e..1ae61075 100644 --- a/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json +++ b/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json @@ -1,16 +1,7 @@ { "description": "toolCallContentChanged sets content on a running tool call", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -35,11 +26,15 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallContentChanged", + "type": "chat/toolCallContentChanged", "turnId": "turn-1", "toolCallId": "tc-1", "content": [ @@ -52,15 +47,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -92,6 +78,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json b/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json index 980ec220..6fbfc1ef 100644 --- a/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json +++ b/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json @@ -1,16 +1,7 @@ { "description": "toolCallContentChanged is no-op for non-running tool call", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -37,11 +28,15 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallContentChanged", + "type": "chat/toolCallContentChanged", "turnId": "turn-1", "toolCallId": "tc-1", "content": [ @@ -54,15 +49,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -89,6 +75,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json b/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json index 42c18e5e..ff3acddb 100644 --- a/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json +++ b/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json @@ -1,16 +1,7 @@ { "description": "toolCallContentChanged replaces existing content on a running tool call", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -42,11 +33,15 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallContentChanged", + "type": "chat/toolCallContentChanged", "turnId": "turn-1", "toolCallId": "tc-1", "content": [ @@ -63,15 +58,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -107,6 +93,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/088-session-unknown-action-type-is-no-op.json b/types/test-cases/reducers/088-session-unknown-action-type-is-no-op.json index 478e2f1b..c12d9f4f 100644 --- a/types/test-cases/reducers/088-session-unknown-action-type-is-no-op.json +++ b/types/test-cases/reducers/088-session-unknown-action-type-is-no-op.json @@ -11,7 +11,7 @@ "modifiedAt": 2000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -28,6 +28,6 @@ "modifiedAt": 2000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json b/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json index 70c89e50..6f4fca22 100644 --- a/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "toolCallDelta with wrong turnId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -32,26 +23,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallDelta", + "type": "chat/toolCallDelta", "turnId": "wrong-turn", "toolCallId": "tc-1", "content": "data" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -73,6 +59,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json b/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json index 3a7120d7..db7e3c87 100644 --- a/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "responsePart with wrong turnId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/responsePart", + "type": "chat/responsePart", "turnId": "wrong-turn", "part": { "kind": "markdown", @@ -36,15 +31,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -56,6 +42,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json b/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json index 5274894d..64d7f700 100644 --- a/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "toolCallStart with wrong turnId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "wrong-turn", "toolCallId": "tc-1", "toolName": "bash", @@ -34,15 +29,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -54,6 +40,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json b/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json index 11ff8788..cb26096f 100644 --- a/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "usage with wrong turnId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/usage", + "type": "chat/usage", "turnId": "wrong-turn", "usage": { "inputTokens": 100, @@ -35,15 +30,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -55,6 +41,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json b/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json index dd0ba386..7c564a93 100644 --- a/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json +++ b/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "delta with non-existent partId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -28,26 +19,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/delta", + "type": "chat/delta", "turnId": "turn-1", "partId": "nonexistent", "content": "data" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -65,6 +51,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json b/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json index 8f318949..d24f93ab 100644 --- a/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json @@ -1,16 +1,7 @@ { "description": "toolCallDelta on non-streaming tool call is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -35,26 +26,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallDelta", + "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-1", "content": "extra data" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -79,6 +65,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json b/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json index 2928d571..bca47e6a 100644 --- a/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json @@ -1,16 +1,7 @@ { "description": "toolCallConfirmed on non-pending tool call is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -35,11 +26,15 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, @@ -47,15 +42,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -80,6 +66,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json b/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json index dc2c8a7e..e0a00a7f 100644 --- a/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json @@ -1,16 +1,7 @@ { "description": "toolCallComplete on non-running non-pending tool call is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -37,11 +28,15 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -51,15 +46,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -86,6 +72,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json b/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json index 05a9e527..30669340 100644 --- a/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json @@ -1,16 +1,7 @@ { "description": "toolCallResultConfirmed on non-pending-result tool call is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -35,26 +26,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallResultConfirmed", + "type": "chat/toolCallResultConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -79,6 +65,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json index 9cfceeb6..078187c4 100644 --- a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json +++ b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json @@ -1,16 +1,7 @@ { "description": "endTurn force-cancels running tool call with non-streaming fields", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -35,24 +26,19 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/turnComplete", + "type": "chat/turnComplete", "turnId": "turn-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "turn-1", @@ -83,6 +69,10 @@ "error": null } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json b/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json index bb6f9f1c..79b2e8d6 100644 --- a/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json +++ b/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json @@ -1,16 +1,7 @@ { "description": "delta targeting a tool call partId is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -35,26 +26,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/delta", + "type": "chat/delta", "turnId": "turn-1", "partId": "tc-1", "content": "data" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -79,6 +65,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json b/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json index 49d49308..ce34f7e2 100644 --- a/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json +++ b/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json @@ -1,16 +1,7 @@ { "description": "toolCallDelta without invocationMessage preserves existing", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -33,26 +24,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallDelta", + "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-1", "content": "data" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -76,6 +62,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json b/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json index 538893f6..f2121787 100644 --- a/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json +++ b/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json @@ -1,16 +1,7 @@ { "description": "reasoning targeting non-reasoning part is no-op", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -28,26 +19,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/reasoning", + "type": "chat/reasoning", "turnId": "turn-1", "partId": "md-1", "content": "thinking..." } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -65,6 +51,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/103-delta-skips-parts-without-id.json b/types/test-cases/reducers/103-delta-skips-parts-without-id.json index 0cbb7bc8..c205564e 100644 --- a/types/test-cases/reducers/103-delta-skips-parts-without-id.json +++ b/types/test-cases/reducers/103-delta-skips-parts-without-id.json @@ -1,16 +1,7 @@ { "description": "delta skips response parts without id field", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -31,26 +22,21 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/delta", + "type": "chat/delta", "turnId": "turn-1", "partId": "md-1", "content": " new" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -71,6 +57,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json b/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json index 42c72946..d0e5be92 100644 --- a/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json +++ b/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json @@ -1,21 +1,16 @@ { "description": "session input requests sync drafts and restore in-progress status after completion", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/turnStarted", + "type": "chat/turnStarted", "turnId": "turn-1", "message": { "text": "Hello", @@ -25,7 +20,7 @@ } }, { - "type": "session/inputRequested", + "type": "chat/inputRequested", "request": { "id": "input-1", "message": "Clarify requirements", @@ -54,7 +49,7 @@ } }, { - "type": "session/inputAnswerChanged", + "type": "chat/inputAnswerChanged", "requestId": "input-1", "questionId": "q1", "answer": { @@ -66,7 +61,7 @@ } }, { - "type": "session/inputAnswerChanged", + "type": "chat/inputAnswerChanged", "requestId": "input-1", "questionId": "q2", "answer": { @@ -78,21 +73,12 @@ } }, { - "type": "session/inputCompleted", + "type": "chat/inputCompleted", "requestId": "input-1", "response": "accept" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -104,6 +90,10 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json b/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json index 59fa3300..bd66b38c 100644 --- a/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json +++ b/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json @@ -1,16 +1,7 @@ { "description": "session/inputRequested sets inputNeeded status and syncs draft answers", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/inputRequested", + "type": "chat/inputRequested", "request": { "id": "input-1", "message": "Clarify requirements", @@ -40,7 +35,7 @@ } }, { - "type": "session/inputAnswerChanged", + "type": "chat/inputAnswerChanged", "requestId": "input-1", "questionId": "q1", "answer": { @@ -53,15 +48,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -95,6 +81,10 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json b/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json index 10cddd87..b7c5236c 100644 --- a/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json +++ b/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json @@ -1,16 +1,7 @@ { "description": "turn end clears outstanding session input requests", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -40,24 +31,19 @@ } ] } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/turnComplete", + "type": "chat/turnComplete", "turnId": "turn-1" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "turn-1", @@ -73,6 +59,10 @@ "error": null } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json b/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json index aa35e803..64523607 100644 --- a/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json +++ b/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json @@ -1,16 +1,7 @@ { "description": "session input upsert preserves or replaces answers and answerChanged clears last draft", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -37,18 +28,22 @@ } } } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/inputRequested", + "type": "chat/inputRequested", "request": { "id": "input-1", "message": "New message" } }, { - "type": "session/inputRequested", + "type": "chat/inputRequested", "request": { "id": "input-1", "message": "New message", @@ -60,21 +55,12 @@ } }, { - "type": "session/inputAnswerChanged", + "type": "chat/inputAnswerChanged", "requestId": "input-1", "questionId": "q2" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -93,6 +79,10 @@ "message": "New message", "answers": null } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/109-session-input-unknown-actions-are-no-op.json b/types/test-cases/reducers/109-session-input-unknown-actions-are-no-op.json index 6b843b59..fa1d0dff 100644 --- a/types/test-cases/reducers/109-session-input-unknown-actions-are-no-op.json +++ b/types/test-cases/reducers/109-session-input-unknown-actions-are-no-op.json @@ -1,21 +1,16 @@ { "description": "session input answer and completion actions are no-op for unknown requests", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" }, "actions": [ { - "type": "session/inputAnswerChanged", + "type": "chat/inputAnswerChanged", "requestId": "missing", "questionId": "q1", "answer": { @@ -23,21 +18,16 @@ } }, { - "type": "session/inputCompleted", + "type": "chat/inputCompleted", "requestId": "missing", "response": "cancel" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 1000 - }, - "lifecycle": "ready", - "turns": [] + "turns": [], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z" } } diff --git a/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json b/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json index d6bb83c4..8987abf0 100644 --- a/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json +++ b/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json @@ -1,16 +1,7 @@ { "description": "session input completion and truncation remove outstanding requests", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [ { "id": "kept", @@ -58,29 +49,24 @@ "message": "Remaining request", "url": "https://example.com/form" } - ] + ], + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/inputCompleted", + "type": "chat/inputCompleted", "requestId": "complete-me", "response": "decline" }, { - "type": "session/truncated", + "type": "chat/truncated", "turnId": "kept" } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 1, - "createdAt": 1000, - "modifiedAt": 9999 - }, - "lifecycle": "ready", "turns": [ { "id": "kept", @@ -95,6 +81,10 @@ "state": "complete" } ], - "activeTurn": null + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" } } diff --git a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json index 85cc1863..26661966 100644 --- a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json +++ b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json @@ -1,16 +1,7 @@ { "description": "tool call awaiting confirmation sets session status to input-needed", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: rm -rf /tmp/test", @@ -41,15 +36,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -78,6 +64,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json b/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json index 375f7512..36e2a30a 100644 --- a/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json +++ b/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json @@ -1,16 +1,7 @@ { "description": "tool call pending result confirmation sets session status to input-needed", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,25 +13,29 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run", "confirmed": "not-needed" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -51,15 +46,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -88,6 +74,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/113-session-configchanged-merges-into-config-values.json b/types/test-cases/reducers/113-session-configchanged-merges-into-config-values.json index 63f46c0a..23e47469 100644 --- a/types/test-cases/reducers/113-session-configchanged-merges-into-config-values.json +++ b/types/test-cases/reducers/113-session-configchanged-merges-into-config-values.json @@ -38,7 +38,7 @@ } }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -85,6 +85,6 @@ } }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/114-session-configchanged-noops-when-config-undefined.json b/types/test-cases/reducers/114-session-configchanged-noops-when-config-undefined.json index 19b4c27c..412f0d6c 100644 --- a/types/test-cases/reducers/114-session-configchanged-noops-when-config-undefined.json +++ b/types/test-cases/reducers/114-session-configchanged-noops-when-config-undefined.json @@ -11,7 +11,7 @@ "modifiedAt": 2000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -31,6 +31,6 @@ "modifiedAt": 2000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/126-session-modelchanged-with-modelconfig.json b/types/test-cases/reducers/126-session-modelchanged-with-modelconfig.json index ddac6734..3b78b2d1 100644 --- a/types/test-cases/reducers/126-session-modelchanged-with-modelconfig.json +++ b/types/test-cases/reducers/126-session-modelchanged-with-modelconfig.json @@ -14,7 +14,7 @@ } }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -43,6 +43,6 @@ } }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json index 06aadb19..786f8e0f 100644 --- a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json +++ b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json @@ -1,16 +1,7 @@ { "description": "toolCallReady with options preserves them in pending-confirmation state", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: echo hello", @@ -66,15 +61,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -129,6 +115,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json b/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json index 759ebd19..01ba9372 100644 --- a/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json +++ b/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json @@ -1,16 +1,7 @@ { "description": "toolCallConfirmed approved with selectedOptionId resolves selectedOption in running state", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: echo hello", @@ -59,7 +54,7 @@ ] }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, @@ -68,15 +63,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -109,6 +95,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/129-session-configchanged-replace-replaces-all-values.json b/types/test-cases/reducers/129-session-configchanged-replace-replaces-all-values.json index a4120002..29fc2907 100644 --- a/types/test-cases/reducers/129-session-configchanged-replace-replaces-all-values.json +++ b/types/test-cases/reducers/129-session-configchanged-replace-replaces-all-values.json @@ -38,7 +38,7 @@ } }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -85,6 +85,6 @@ } }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json b/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json index 91d9eb76..1320e5ab 100644 --- a/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json +++ b/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json @@ -1,16 +1,7 @@ { "description": "toolCallConfirmed denied with selectedOptionId resolves selectedOption in cancelled state", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: rm -rf /", @@ -59,7 +54,7 @@ ] }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": false, @@ -69,15 +64,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -112,6 +98,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json b/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json index e685e0ba..7f6938d9 100644 --- a/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json +++ b/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json @@ -1,16 +1,7 @@ { "description": "selectedOption carries through from running to completed via toolCallComplete", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: echo hello", @@ -53,7 +48,7 @@ ] }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, @@ -61,7 +56,7 @@ "selectedOptionId": "approve-session" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -71,15 +66,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -114,6 +100,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json b/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json index cfeee2f1..63ea7420 100644 --- a/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json +++ b/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json @@ -1,16 +1,7 @@ { "description": "selectedOption carries through pending-result-confirmation approved to completed", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: echo hello", @@ -47,7 +42,7 @@ ] }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, @@ -55,7 +50,7 @@ "selectedOptionId": "approve-session" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -65,22 +60,13 @@ "requiresResultConfirmation": true }, { - "type": "session/toolCallResultConfirmed", + "type": "chat/toolCallResultConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -118,6 +104,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json b/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json index 7c7d9351..44be523e 100644 --- a/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json +++ b/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json @@ -1,16 +1,7 @@ { "description": "selectedOption carries through result-denied cancellation", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,18 +13,22 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: echo hello", @@ -47,7 +42,7 @@ ] }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, @@ -55,7 +50,7 @@ "selectedOptionId": "approve-once" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -65,22 +60,13 @@ "requiresResultConfirmation": true }, { - "type": "session/toolCallResultConfirmed", + "type": "chat/toolCallResultConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": false } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -113,6 +99,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/133-session-activitychanged-sets-activity.json b/types/test-cases/reducers/133-session-activitychanged-sets-activity.json index 6a95a2e4..c1028260 100644 --- a/types/test-cases/reducers/133-session-activitychanged-sets-activity.json +++ b/types/test-cases/reducers/133-session-activitychanged-sets-activity.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -30,6 +30,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/134-session-activitychanged-clears-activity.json b/types/test-cases/reducers/134-session-activitychanged-clears-activity.json index 5981cbac..33b7c8d4 100644 --- a/types/test-cases/reducers/134-session-activitychanged-clears-activity.json +++ b/types/test-cases/reducers/134-session-activitychanged-clears-activity.json @@ -12,7 +12,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -31,6 +31,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/135-session-metachanged-sets-meta.json b/types/test-cases/reducers/135-session-metachanged-sets-meta.json index a6516c21..6b2c4363 100644 --- a/types/test-cases/reducers/135-session-metachanged-sets-meta.json +++ b/types/test-cases/reducers/135-session-metachanged-sets-meta.json @@ -11,12 +11,12 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], "_meta": { "git": { "branch": "main" } - } + }, + "chats": [] }, "actions": [ { @@ -39,12 +39,12 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], "_meta": { "git": { "branch": "feature" }, "vscode.foo": 42 - } + }, + "chats": [] } } diff --git a/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json b/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json index 05df95ff..77e692dd 100644 --- a/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json +++ b/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -19,7 +18,9 @@ "uri": "https://plugins.example/a", "name": "Plugin A", "enabled": true, - "load": { "kind": "loading" } + "load": { + "kind": "loading" + } }, { "type": "plugin", @@ -27,9 +28,12 @@ "uri": "https://plugins.example/b", "name": "Plugin B", "enabled": true, - "load": { "kind": "loaded" } + "load": { + "kind": "loaded" + } } - ] + ], + "chats": [] }, "actions": [ { @@ -40,7 +44,10 @@ "uri": "https://plugins.example/a", "name": "Plugin A", "enabled": true, - "load": { "kind": "error", "message": "Failed to load" } + "load": { + "kind": "error", + "message": "Failed to load" + } } } ], @@ -54,7 +61,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -62,7 +68,10 @@ "uri": "https://plugins.example/a", "name": "Plugin A", "enabled": true, - "load": { "kind": "error", "message": "Failed to load" } + "load": { + "kind": "error", + "message": "Failed to load" + } }, { "type": "plugin", @@ -70,8 +79,11 @@ "uri": "https://plugins.example/b", "name": "Plugin B", "enabled": true, - "load": { "kind": "loaded" } + "load": { + "kind": "loaded" + } } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json b/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json index 378de3e6..a49ecd7c 100644 --- a/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json +++ b/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -20,7 +19,8 @@ "name": "Plugin A", "enabled": true } - ] + ], + "chats": [] }, "actions": [ { @@ -31,7 +31,9 @@ "uri": "https://plugins.example/new", "name": "New Plugin", "enabled": true, - "load": { "kind": "loading" } + "load": { + "kind": "loading" + } } } ], @@ -45,7 +47,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -60,8 +61,11 @@ "uri": "https://plugins.example/new", "name": "New Plugin", "enabled": true, - "load": { "kind": "loading" } + "load": { + "kind": "loading" + } } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/139-session-customizationupdated-creates-list.json b/types/test-cases/reducers/139-session-customizationupdated-creates-list.json index f537b0e5..c17e124c 100644 --- a/types/test-cases/reducers/139-session-customizationupdated-creates-list.json +++ b/types/test-cases/reducers/139-session-customizationupdated-creates-list.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -37,7 +37,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "directory", @@ -48,6 +47,7 @@ "contents": "skill", "writable": true } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/145-session-changesetschanged-sets-catalogue.json b/types/test-cases/reducers/145-session-changesetschanged-sets-catalogue.json index 57397945..1592828f 100644 --- a/types/test-cases/reducers/145-session-changesetschanged-sets-catalogue.json +++ b/types/test-cases/reducers/145-session-changesetschanged-sets-catalogue.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] }, "actions": [ { @@ -35,13 +35,13 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], "changesets": [ { "label": "Session Changes", "uriTemplate": "copilot:/test-session/changeset/session", "changeKind": "session" } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/146-session-changesetschanged-clears-catalogue.json b/types/test-cases/reducers/146-session-changesetschanged-clears-catalogue.json index f99a5804..9c119c7c 100644 --- a/types/test-cases/reducers/146-session-changesetschanged-clears-catalogue.json +++ b/types/test-cases/reducers/146-session-changesetschanged-clears-catalogue.json @@ -11,14 +11,14 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], "changesets": [ { "label": "Session Changes", "uriTemplate": "copilot:/test-session/changeset/session", "changeKind": "session" } - ] + ], + "chats": [] }, "actions": [ { @@ -36,6 +36,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/147-session-agentchanged-sets-agent.json b/types/test-cases/reducers/147-session-agentchanged-sets-agent.json index bcb1b276..f97f0d4f 100644 --- a/types/test-cases/reducers/147-session-agentchanged-sets-agent.json +++ b/types/test-cases/reducers/147-session-agentchanged-sets-agent.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -34,6 +34,6 @@ } }, "lifecycle": "creating", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/147-session-ready-preserves-inprogress-status.json b/types/test-cases/reducers/147-session-ready-preserves-inprogress-status.json index 5685e4bd..da95e868 100644 --- a/types/test-cases/reducers/147-session-ready-preserves-inprogress-status.json +++ b/types/test-cases/reducers/147-session-ready-preserves-inprogress-status.json @@ -11,18 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], - "activeTurn": { - "id": "turn-1", - "message": { - "text": "Hello", - "origin": { - "kind": "user" - } - }, - "responseParts": [], - "usage": null - } + "chats": [] }, "actions": [ { @@ -39,17 +28,6 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], - "activeTurn": { - "id": "turn-1", - "message": { - "text": "Hello", - "origin": { - "kind": "user" - } - }, - "responseParts": [], - "usage": null - } + "chats": [] } } diff --git a/types/test-cases/reducers/148-session-agentchanged-clears-agent.json b/types/test-cases/reducers/148-session-agentchanged-clears-agent.json index 9db0ab76..7b86ca22 100644 --- a/types/test-cases/reducers/148-session-agentchanged-clears-agent.json +++ b/types/test-cases/reducers/148-session-agentchanged-clears-agent.json @@ -14,7 +14,7 @@ } }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -32,6 +32,6 @@ "agent": null }, "lifecycle": "creating", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/149-session-agentchanged-replaces-agent.json b/types/test-cases/reducers/149-session-agentchanged-replaces-agent.json index c3c8736e..f8ec4acb 100644 --- a/types/test-cases/reducers/149-session-agentchanged-replaces-agent.json +++ b/types/test-cases/reducers/149-session-agentchanged-replaces-agent.json @@ -14,7 +14,7 @@ } }, "lifecycle": "creating", - "turns": [] + "chats": [] }, "actions": [ { @@ -37,6 +37,6 @@ } }, "lifecycle": "creating", - "turns": [] + "chats": [] } } diff --git a/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json b/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json index e96e2332..c0676061 100644 --- a/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json +++ b/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -35,7 +34,8 @@ "name": "Plugin B", "enabled": true } - ] + ], + "chats": [] }, "actions": [ { @@ -53,7 +53,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -62,6 +61,7 @@ "name": "Plugin B", "enabled": true } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/153-session-customizationremoved-removes-child.json b/types/test-cases/reducers/153-session-customizationremoved-removes-child.json index 380a27d8..c506cefc 100644 --- a/types/test-cases/reducers/153-session-customizationremoved-removes-child.json +++ b/types/test-cases/reducers/153-session-customizationremoved-removes-child.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -34,7 +33,8 @@ } ] } - ] + ], + "chats": [] }, "actions": [ { @@ -52,7 +52,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -69,6 +68,7 @@ } ] } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json b/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json index ee6d6b22..777a5793 100644 --- a/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json +++ b/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json @@ -11,7 +11,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -20,7 +19,8 @@ "name": "Plugin A", "enabled": true } - ] + ], + "chats": [] }, "actions": [ { @@ -38,7 +38,6 @@ "modifiedAt": 1000 }, "lifecycle": "creating", - "turns": [], "customizations": [ { "type": "plugin", @@ -47,6 +46,7 @@ "name": "Plugin A", "enabled": true } - ] + ], + "chats": [] } } diff --git a/types/test-cases/reducers/156-session-default-chat-changed.json b/types/test-cases/reducers/156-session-default-chat-changed.json new file mode 100644 index 00000000..564f55b6 --- /dev/null +++ b/types/test-cases/reducers/156-session-default-chat-changed.json @@ -0,0 +1,36 @@ +{ + "description": "session/defaultChatChanged updates input-routing hint", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [], + "defaultChat": "ahp-chat:/old" + }, + "actions": [ + { + "type": "session/defaultChatChanged", + "defaultChat": "ahp-chat:/new" + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [], + "defaultChat": "ahp-chat:/new" + } +} diff --git a/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json b/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json index af97721f..9afbae97 100644 --- a/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json +++ b/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json @@ -1,16 +1,7 @@ { "description": "toolCallConfirmed approved with editedToolInput overrides the original toolInput in the running state", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,25 +13,29 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "bash", "displayName": "Run Command" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: ls -la", "toolInput": "ls -la" }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-1", "approved": true, @@ -48,7 +43,7 @@ "editedToolInput": "ls -la /tmp" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -58,15 +53,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -95,6 +81,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json b/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json index 2ea7247a..717f6a46 100644 --- a/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json +++ b/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "mcpServer", @@ -19,7 +19,9 @@ "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", "enabled": true, - "state": { "kind": "starting" } + "state": { + "kind": "starting" + } } ] }, @@ -27,7 +29,9 @@ { "type": "session/mcpServerStateChanged", "id": "mcp-1", - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://filesystem" } ], @@ -41,7 +45,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "mcpServer", @@ -49,7 +53,9 @@ "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", "enabled": true, - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://filesystem" } ] diff --git a/types/test-cases/reducers/160-session-default-chat-changed-unsets.json b/types/test-cases/reducers/160-session-default-chat-changed-unsets.json new file mode 100644 index 00000000..1766a487 --- /dev/null +++ b/types/test-cases/reducers/160-session-default-chat-changed-unsets.json @@ -0,0 +1,35 @@ +{ + "description": "session/defaultChatChanged unsets input-routing hint", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [], + "defaultChat": "ahp-chat:/old" + }, + "actions": [ + { + "type": "session/defaultChatChanged" + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [], + "defaultChat": null + } +} diff --git a/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json b/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json index 7218521a..5e674318 100644 --- a/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json +++ b/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "plugin", @@ -32,7 +32,9 @@ "uri": "https://plugins.example/a#mcp/search", "name": "Search", "enabled": true, - "state": { "kind": "starting" } + "state": { + "kind": "starting" + } } ] } @@ -42,7 +44,9 @@ { "type": "session/mcpServerStateChanged", "id": "mcp-child", - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://search" } ], @@ -56,7 +60,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "plugin", @@ -77,7 +81,9 @@ "uri": "https://plugins.example/a#mcp/search", "name": "Search", "enabled": true, - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://search" } ] diff --git a/types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json b/types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json new file mode 100644 index 00000000..df65d14c --- /dev/null +++ b/types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json @@ -0,0 +1,76 @@ +{ + "description": "chat turn lifecycle starts, streams, and completes on a chat channel", + "reducer": "chat", + "initial": { + "turns": [], + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { + "kind": "user" + } + }, + "actions": [ + { + "type": "chat/turnStarted", + "turnId": "turn-1", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + } + }, + { + "type": "chat/responsePart", + "turnId": "turn-1", + "part": { + "kind": "markdown", + "id": "md-1", + "content": "" + } + }, + { + "type": "chat/delta", + "turnId": "turn-1", + "partId": "md-1", + "content": "Hello from chat" + }, + { + "type": "chat/turnComplete", + "turnId": "turn-1" + } + ], + "expected": { + "turns": [ + { + "id": "turn-1", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "markdown", + "id": "md-1", + "content": "Hello from chat" + } + ], + "usage": null, + "state": "complete", + "error": null + } + ], + "activeTurn": null, + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z", + "origin": { + "kind": "user" + } + } +} diff --git a/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json b/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json index e9712e57..a4b5c948 100644 --- a/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json +++ b/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "mcpServer", @@ -19,7 +19,9 @@ "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", "enabled": true, - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://filesystem" } ] @@ -28,7 +30,9 @@ { "type": "session/mcpServerStateChanged", "id": "mcp-does-not-exist", - "state": { "kind": "stopped" } + "state": { + "kind": "stopped" + } } ], "expected": { @@ -41,7 +45,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "mcpServer", @@ -49,7 +53,9 @@ "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", "enabled": true, - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://filesystem" } ] diff --git a/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json b/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json index 0dd4e187..532658a0 100644 --- a/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json +++ b/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json @@ -11,7 +11,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "plugin", @@ -34,13 +34,17 @@ { "type": "session/mcpServerStateChanged", "id": "plugin-a", - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://nope" }, { "type": "session/mcpServerStateChanged", "id": "skill-1", - "state": { "kind": "ready" }, + "state": { + "kind": "ready" + }, "channel": "mcp://nope" } ], @@ -54,7 +58,7 @@ "modifiedAt": 1000 }, "lifecycle": "ready", - "turns": [], + "chats": [], "customizations": [ { "type": "plugin", diff --git a/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json b/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json index 3e8d40cb..cbe1b13d 100644 --- a/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json +++ b/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json @@ -1,16 +1,7 @@ { - "description": "session/toolCallStart with a non-null mcp contributor carries the contributor through tcBase across ready and complete", - "reducer": "session", + "description": "chat/toolCallStart with a non-null mcp contributor carries the contributor through tcBase across ready and complete", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-1", "toolName": "search", @@ -37,7 +32,7 @@ } }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Search: foo", @@ -45,7 +40,7 @@ "confirmed": "not-needed" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-1", "result": { @@ -55,15 +50,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -95,6 +81,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" } } diff --git a/types/test-cases/reducers/170-session-chatadded-appends.json b/types/test-cases/reducers/170-session-chatadded-appends.json new file mode 100644 index 00000000..781d920e --- /dev/null +++ b/types/test-cases/reducers/170-session-chatadded-appends.json @@ -0,0 +1,48 @@ +{ + "description": "session/chatAdded appends a new chat to the catalog", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [] + }, + "actions": [ + { + "type": "session/chatAdded", + "summary": { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + ] + } +} diff --git a/types/test-cases/reducers/171-session-chatadded-upserts.json b/types/test-cases/reducers/171-session-chatadded-upserts.json new file mode 100644 index 00000000..efe8d3ed --- /dev/null +++ b/types/test-cases/reducers/171-session-chatadded-upserts.json @@ -0,0 +1,56 @@ +{ + "description": "session/chatAdded upserts an existing chat by resource", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + ] + }, + "actions": [ + { + "type": "session/chatAdded", + "summary": { + "resource": "ahp-chat:/c1", + "title": "Chat 1 (renamed)", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z", + "origin": { "kind": "user" } + } + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1 (renamed)", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z", + "origin": { "kind": "user" } + } + ] + } +} diff --git a/types/test-cases/reducers/172-session-chatremoved.json b/types/test-cases/reducers/172-session-chatremoved.json new file mode 100644 index 00000000..7a9cfed6 --- /dev/null +++ b/types/test-cases/reducers/172-session-chatremoved.json @@ -0,0 +1,60 @@ +{ + "description": "session/chatRemoved removes a chat and clears defaultChat when it matches", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + }, + { + "resource": "ahp-chat:/c2", + "title": "Chat 2", + "status": 8, + "activity": "Thinking", + "modifiedAt": "1970-01-01T00:00:02.000Z", + "origin": { "kind": "fork", "chat": "ahp-chat:/c1", "turnId": "t1" } + } + ], + "defaultChat": "ahp-chat:/c1" + }, + "actions": [ + { + "type": "session/chatRemoved", + "chat": "ahp-chat:/c1" + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c2", + "title": "Chat 2", + "status": 8, + "activity": "Thinking", + "modifiedAt": "1970-01-01T00:00:02.000Z", + "origin": { "kind": "fork", "chat": "ahp-chat:/c1", "turnId": "t1" } + } + ] + } +} diff --git a/types/test-cases/reducers/173-session-chatupdated.json b/types/test-cases/reducers/173-session-chatupdated.json new file mode 100644 index 00000000..0e134492 --- /dev/null +++ b/types/test-cases/reducers/173-session-chatupdated.json @@ -0,0 +1,56 @@ +{ + "description": "session/chatUpdated merges partial changes into an existing chat", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + ] + }, + "actions": [ + { + "type": "session/chatUpdated", + "chat": "ahp-chat:/c1", + "changes": { + "status": 24, + "activity": "Waiting for approval", + "modifiedAt": "1970-01-01T00:00:02.000Z" + } + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 24, + "activity": "Waiting for approval", + "modifiedAt": "1970-01-01T00:00:02.000Z", + "origin": { "kind": "user" } + } + ] + } +} diff --git a/types/test-cases/reducers/174-session-chatremoved-noop.json b/types/test-cases/reducers/174-session-chatremoved-noop.json new file mode 100644 index 00000000..87c81019 --- /dev/null +++ b/types/test-cases/reducers/174-session-chatremoved-noop.json @@ -0,0 +1,52 @@ +{ + "description": "session/chatRemoved is a no-op when the chat URI is unknown", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + ], + "defaultChat": "ahp-chat:/c1" + }, + "actions": [ + { + "type": "session/chatRemoved", + "chat": "ahp-chat:/cX" + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + ], + "defaultChat": "ahp-chat:/c1" + } +} diff --git a/types/test-cases/reducers/175-session-chatupdated-noop.json b/types/test-cases/reducers/175-session-chatupdated-noop.json new file mode 100644 index 00000000..d262b6c7 --- /dev/null +++ b/types/test-cases/reducers/175-session-chatupdated-noop.json @@ -0,0 +1,51 @@ +{ + "description": "session/chatUpdated is a no-op when the chat URI is unknown", + "reducer": "session", + "initial": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + ] + }, + "actions": [ + { + "type": "session/chatUpdated", + "chat": "ahp-chat:/cX", + "changes": { "title": "Never written" } + } + ], + "expected": { + "summary": { + "resource": "ahp-session:/s1", + "provider": "copilot", + "title": "Session", + "status": 1, + "createdAt": 1000, + "modifiedAt": 1000 + }, + "lifecycle": "ready", + "chats": [ + { + "resource": "ahp-chat:/c1", + "title": "Chat 1", + "status": 1, + "modifiedAt": "1970-01-01T00:00:01.000Z", + "origin": { "kind": "user" } + } + ] + } +} diff --git a/types/test-cases/reducers/220-toolcall-actions-update-meta.json b/types/test-cases/reducers/220-toolcall-actions-update-meta.json index e173a13e..fbabd4a0 100644 --- a/types/test-cases/reducers/220-toolcall-actions-update-meta.json +++ b/types/test-cases/reducers/220-toolcall-actions-update-meta.json @@ -1,16 +1,7 @@ { "description": "tool-call-scoped actions update tool call _meta", - "reducer": "session", + "reducer": "chat", "initial": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 8, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -22,11 +13,15 @@ }, "responseParts": [], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" }, "actions": [ { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-delta", "toolName": "stream", @@ -36,7 +31,7 @@ } }, { - "type": "session/toolCallDelta", + "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-delta", "content": "abc", @@ -46,14 +41,14 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-ready-running", "toolName": "run", "displayName": "Run Tool" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-ready-running", "invocationMessage": "Run now", @@ -64,14 +59,14 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-ready-pending", "toolName": "ask", "displayName": "Ask Tool" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-ready-pending", "invocationMessage": "Needs approval", @@ -80,20 +75,20 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-confirm-approved", "toolName": "approve", "displayName": "Approve Tool" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-confirm-approved", "invocationMessage": "Approve this" }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-confirm-approved", "approved": true, @@ -103,20 +98,20 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-confirm-denied", "toolName": "deny", "displayName": "Deny Tool" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-confirm-denied", "invocationMessage": "Deny this" }, { - "type": "session/toolCallConfirmed", + "type": "chat/toolCallConfirmed", "turnId": "turn-1", "toolCallId": "tc-confirm-denied", "approved": false, @@ -126,21 +121,21 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-complete", "toolName": "complete", "displayName": "Complete Tool" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-complete", "invocationMessage": "Complete this", "confirmed": "not-needed" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-complete", "result": { @@ -152,21 +147,21 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-result-confirmed", "toolName": "result", "displayName": "Result Tool" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-result-confirmed", "invocationMessage": "Result this", "confirmed": "not-needed" }, { - "type": "session/toolCallComplete", + "type": "chat/toolCallComplete", "turnId": "turn-1", "toolCallId": "tc-result-confirmed", "result": { @@ -179,7 +174,7 @@ } }, { - "type": "session/toolCallResultConfirmed", + "type": "chat/toolCallResultConfirmed", "turnId": "turn-1", "toolCallId": "tc-result-confirmed", "approved": true, @@ -188,21 +183,21 @@ } }, { - "type": "session/toolCallStart", + "type": "chat/toolCallStart", "turnId": "turn-1", "toolCallId": "tc-content", "toolName": "content", "displayName": "Content Tool" }, { - "type": "session/toolCallReady", + "type": "chat/toolCallReady", "turnId": "turn-1", "toolCallId": "tc-content", "invocationMessage": "Content this", "confirmed": "not-needed" }, { - "type": "session/toolCallContentChanged", + "type": "chat/toolCallContentChanged", "turnId": "turn-1", "toolCallId": "tc-content", "content": [ @@ -218,15 +213,6 @@ } ], "expected": { - "summary": { - "resource": "copilot:/test-session", - "provider": "copilot", - "title": "Test Session", - "status": 24, - "createdAt": 1000, - "modifiedAt": 2000 - }, - "lifecycle": "ready", "turns": [], "activeTurn": { "id": "turn-1", @@ -240,7 +226,6 @@ { "kind": "toolCall", "toolCall": { - "status": "streaming", "toolCallId": "tc-delta", "toolName": "stream", "displayName": "Stream Tool", @@ -248,6 +233,7 @@ "_meta": { "phase": "delta" }, + "status": "streaming", "partialInput": "abc", "invocationMessage": "Streaming input" } @@ -384,6 +370,10 @@ } ], "usage": null - } + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" } -} \ No newline at end of file +} diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index a97ee2f1..414602d6 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -56,6 +56,8 @@ type _ExpectedCommands = | 'subscribe' | 'createSession' | 'disposeSession' + | 'createChat' + | 'disposeChat' | 'createTerminal' | 'disposeTerminal' | 'createResourceWatch' diff --git a/types/version/registry.ts b/types/version/registry.ts index 61de3784..8a0ce6e3 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -80,45 +80,49 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.RootActiveSessionsChanged]: '0.1.0', [ActionType.SessionReady]: '0.1.0', [ActionType.SessionCreationFailed]: '0.1.0', - [ActionType.SessionTurnStarted]: '0.1.0', - [ActionType.SessionDelta]: '0.1.0', - [ActionType.SessionResponsePart]: '0.1.0', - [ActionType.SessionToolCallStart]: '0.1.0', - [ActionType.SessionToolCallDelta]: '0.1.0', - [ActionType.SessionToolCallReady]: '0.1.0', - [ActionType.SessionToolCallConfirmed]: '0.1.0', - [ActionType.SessionToolCallComplete]: '0.1.0', - [ActionType.SessionToolCallResultConfirmed]: '0.1.0', - [ActionType.SessionToolCallContentChanged]: '0.1.0', - [ActionType.SessionTurnComplete]: '0.1.0', - [ActionType.SessionTurnCancelled]: '0.1.0', - [ActionType.SessionError]: '0.1.0', + [ActionType.SessionChatAdded]: '0.4.0', + [ActionType.SessionChatRemoved]: '0.4.0', + [ActionType.SessionChatUpdated]: '0.4.0', + [ActionType.SessionDefaultChatChanged]: '0.4.0', [ActionType.SessionTitleChanged]: '0.1.0', - [ActionType.SessionUsage]: '0.1.0', - [ActionType.SessionReasoning]: '0.1.0', [ActionType.SessionModelChanged]: '0.1.0', [ActionType.SessionAgentChanged]: '0.2.0', [ActionType.SessionServerToolsChanged]: '0.1.0', [ActionType.SessionActiveClientChanged]: '0.1.0', [ActionType.SessionActiveClientToolsChanged]: '0.1.0', - [ActionType.SessionPendingMessageSet]: '0.1.0', - [ActionType.SessionPendingMessageRemoved]: '0.1.0', - [ActionType.SessionQueuedMessagesReordered]: '0.1.0', - [ActionType.SessionInputRequested]: '0.1.0', - [ActionType.SessionInputAnswerChanged]: '0.1.0', - [ActionType.SessionInputCompleted]: '0.1.0', [ActionType.SessionCustomizationsChanged]: '0.1.0', [ActionType.SessionCustomizationToggled]: '0.1.0', [ActionType.SessionCustomizationUpdated]: '0.1.0', [ActionType.SessionCustomizationRemoved]: '0.2.0', [ActionType.SessionMcpServerStateChanged]: '0.3.0', - [ActionType.SessionTruncated]: '0.1.0', [ActionType.SessionIsReadChanged]: '0.1.0', [ActionType.SessionIsArchivedChanged]: '0.1.0', [ActionType.SessionActivityChanged]: '0.1.0', [ActionType.SessionChangesetsChanged]: '0.2.0', [ActionType.SessionConfigChanged]: '0.1.0', [ActionType.SessionMetaChanged]: '0.1.0', + [ActionType.ChatTurnStarted]: '0.4.0', + [ActionType.ChatDelta]: '0.4.0', + [ActionType.ChatResponsePart]: '0.4.0', + [ActionType.ChatToolCallStart]: '0.4.0', + [ActionType.ChatToolCallDelta]: '0.4.0', + [ActionType.ChatToolCallReady]: '0.4.0', + [ActionType.ChatToolCallConfirmed]: '0.4.0', + [ActionType.ChatToolCallComplete]: '0.4.0', + [ActionType.ChatToolCallResultConfirmed]: '0.4.0', + [ActionType.ChatToolCallContentChanged]: '0.4.0', + [ActionType.ChatTurnComplete]: '0.4.0', + [ActionType.ChatTurnCancelled]: '0.4.0', + [ActionType.ChatError]: '0.4.0', + [ActionType.ChatUsage]: '0.4.0', + [ActionType.ChatReasoning]: '0.4.0', + [ActionType.ChatPendingMessageSet]: '0.4.0', + [ActionType.ChatPendingMessageRemoved]: '0.4.0', + [ActionType.ChatQueuedMessagesReordered]: '0.4.0', + [ActionType.ChatInputRequested]: '0.4.0', + [ActionType.ChatInputAnswerChanged]: '0.4.0', + [ActionType.ChatInputCompleted]: '0.4.0', + [ActionType.ChatTruncated]: '0.4.0', [ActionType.ChangesetStatusChanged]: '0.2.0', [ActionType.ChangesetFileSet]: '0.2.0', [ActionType.ChangesetFileRemoved]: '0.2.0', From 38cc70d89b7fcc316db18531e86dd1b6afb9f553 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 10 Jun 2026 16:00:04 +0200 Subject: [PATCH 2/3] docs: add multi-chat feature overview proposal Adds a feature-level walkthrough of multi-chat sessions for reviewers and UX exploration. Lives under docs/proposals (not wired into public nav). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/proposals/multi-chat.md | 1040 ++++++++++++++++++++++++++++++++++ 1 file changed, 1040 insertions(+) create mode 100644 docs/proposals/multi-chat.md diff --git a/docs/proposals/multi-chat.md b/docs/proposals/multi-chat.md new file mode 100644 index 00000000..f7577d6a --- /dev/null +++ b/docs/proposals/multi-chat.md @@ -0,0 +1,1040 @@ +# Multiple Chats in a Session — Feature Overview + +> A conceptual walkthrough of the multi-chat feature for presentations and +> design discussion. This document deliberately stays at the *feature* level — +> it explains **what** the capability is and **why** it exists, without going +> into the protocol's concrete actions, state shapes, or wire format. + +--- + +## 1. The problem + +An agent session today is **a single, linear conversation**. The session *is* +the chat: one stream of messages, tool calls, and results between a user and an +agent. + +That model is increasingly at odds with where agent products are heading. +Modern harnesses run **more than one agent at a time**: + +- A lead agent that breaks a feature into subtasks and farms them out to workers. +- A "team" of specialised agents (a reviewer, a test-writer, an implementer) + working in parallel. +- A swarm of workers, each operating in its own checkout of the repository. + +When a single session can only ever be one conversation, a user interface has no +honest way to *show* this. The work either gets flattened into one noisy +transcript, or it gets split across unrelated sessions that lose their shared +context (the same workspace, the same project, the same configuration). + +**The feature in one sentence:** let a single session contain *multiple +concurrent chats* that share one context, so that multi-agent work can be +represented, observed, and interacted with as a coherent whole. + +--- + +## 2. The mental model + +The shift is to stop treating "session" and "conversation" as the same thing, +and split them into two roles: + +- **A session is a coordination *scope*.** It owns everything that is shared: + the workspace, the project, the default model and agent, configuration, and + any customizations. It is the boundary of trust and identity — everything + inside a session is "the same actor working on the same thing." + +- **A chat is a conversation *stream* over that scope.** Each chat is an + independently-followable thread of messages and activity. Chats are created + and removed over the lifetime of the session, and each one can be watched on + its own. + +> Scope vs. stream is the whole idea. One scope, many streams. + +A helpful analogy: a session is a **project workspace**, and chats are the +**individual work threads** happening inside it. Closing one thread doesn't tear +down the workspace; the workspace is what gives every thread its shared ground. + +--- + +## 3. Where this feature lives in the stack + +This is the single most important framing for a reviewer or an audience, because +it answers the obvious follow-up question ("where do the agents talk to each +other?") before it's asked. + +``` + ┌─────────────┐ ┌──────────────────────────────┐ + │ UI client │ ◄── feature layer ──► │ Agent harness │ + │ (the app a │ sessions, chats, │ ── lead agent │ + │ user sees) │ activity, status │ ── worker agents ◄───┐ │ + └─────────────┘ │ ── task routing / │ │ + │ results (internal) ─┘ │ + └──────────────────────────────┘ +``` + +There are two distinct layers, and this feature only touches one of them: + +- **The harness layer** is where the *agents actually run*. Spawning a worker, + giving it a task, routing a message to it, and collecting its result are all + things the harness does inside its own runtime. + +- **The feature/interoperability layer** (where this feature lives) is the + **window** onto that work. Its job is to let a user interface *represent and + interact with* what the harness is doing — to show the chats, stream their + activity, and surface their combined status. + +**The feature is about observability and interaction, not agent runtime.** It +makes multi-agent work *visible and usable*; it does not dictate how agents +coordinate internally. + +--- + +## 4. What the feature gives you + +At the feature level, multi-chat introduces a small number of capabilities: + +1. **A catalog of chats per session.** A session exposes the set of chats it + currently contains. Chats can be added and removed as work spins up and winds + down, and each chat carries a lightweight summary (title, current status, + most recent activity). + +2. **A default chat.** One chat is designated the "primary" thread — the natural + place a user lands, and the fallback the session points at when nothing more + specific applies. + +3. **Independent, subscribable streams.** Each chat can be followed on its own: + its messages, its tool calls, its progress. Watching one chat doesn't require + pulling in the noise of the others. + +4. **Per-chat working directory.** Each chat may pin to its own working + directory. By default a chat inherits the session's, but a chat can override + it — which is exactly what a swarm of workers in separate worktrees needs. + +5. **An aggregated session view.** The session presents a roll-up of its chats: + a combined status (e.g. "needs input" bubbles up if *any* chat is blocked), + an overall activity, and a most-recently-modified timestamp. This lets a UI + show a single, honest summary chip for the whole session. + +```mermaid +flowchart LR + C1["Chat A
idle"] --> Sum + C2["Chat B
needs input"] --> Sum + C3["Chat C
working"] --> Sum + Sum["Session summary
status: NEEDS INPUT ⬅ bubbles up
activity: from primary chat
modified: max across chats"] +``` + +--- + +## 5. Worked example: an agent team + +Consider a harness that runs a "team": a lead agent plus a couple of workers, +each in its own checkout of the repository. + +| What the harness does | How the feature represents it | +| --- | --- | +| Lead agent starts | A session with one default chat (the lead). | +| Lead spins up two workers | Two new chats appear in the session's catalog. | +| Each worker takes its own worktree | Each worker chat pins its own working directory. | +| Workers stream progress | Each chat streams its own activity, watchable on its own. | +| A worker gets stuck and needs input | The session summary rolls up to "needs input." | +| A worker finishes and is torn down | Its chat is removed from the catalog. | + +Crucially, **the team's internal coordination never crosses this layer.** The +lead telling a worker what to do, and the worker returning its result, are the +harness's own business. The feature's job is only to make the team *appear* as a +set of chats a user can watch and step into. + +This is why you can support a full agent-team experience **without any +chat-to-chat communication in the protocol itself**: the communication already +happens, one layer down, inside the harness. + +--- + +## 6. What this feature deliberately is *not* + +Drawing the boundary is as important as the feature itself. + +- **It is not chat-to-chat messaging.** There is no primitive for one chat to + send a message to, or read the stream of, another chat. Chats are independent + streams that happen to share a scope. Coordination between agents is a harness + concern. + +- **It does not model agent hierarchies.** The feature has no notion of + "lead vs. worker," "parent vs. child," or assignments and work items. All + chats are peers under one session. A harness may *use* them to represent a + hierarchy, but the feature stays neutral. + +- **It is not sub-sessions.** Multi-chat is about *cooperating threads that share + one trust/identity/context*. Independent agents with their own lifecycle and + their own handshake are a separate concept entirely. + +These omissions are intentional. Richer coordination — work items, operations, +assignments, cross-agent messaging — is a natural *future* axis that can be added +later without breaking this feature. Multi-chat is designed to **compose with** +that future work, not to pre-empt it. + +The sub-session distinction is worth a picture, since it's the most common point +of confusion. *Multi-chat* is cooperating threads under **one** identity, trust, +and context. *Sub-sessions* are independent agents that authenticate on their +own and have their own lifecycle — a separate, future concept: + +```mermaid +flowchart TB + subgraph Multi["Multi-chat (this feature)"] + direction TB + MS["One session
shared trust · identity · context"] + MS --> MA["Chat"] + MS --> MB["Chat"] + MS --> MC["Chat"] + end + + subgraph Sub["Sub-sessions (a separate, future concept)"] + direction TB + P["Session"] + P -->|own lifecycle + handshake| S1["Independent session"] + P -->|own lifecycle + handshake| S2["Independent session"] + end +``` + +--- + +## 7. Why this shape + +A few principles drove the design: + +- **Represent, don't orchestrate.** Stay an observability-and-interaction layer. + The moment the feature starts routing messages between agents, it stops being a + neutral window and starts being an agent framework — a much larger commitment. + +- **Shared context is the point.** The reason these chats belong together is that + they share a workspace, a project, and a configuration. That shared scope is + what a bundle of unrelated sessions could never give you. + +- **Small, additive, composable.** Each capability (catalog, default, per-chat + working directory, aggregated summary) is a small addition that earns its + place, and leaves room for the bigger coordination story to land later. + +- **Graceful degradation.** The feature is backward compatible: a harness that + only ever runs one conversation exposes exactly **one chat (the default)**, and + the experience is identical to today. As a harness grows richer multi-agent + behaviour, it simply lights up *more chats* in the same session — there is + nothing to opt out of, and nothing breaks. + +```mermaid +flowchart LR + A["Single-chat harness
(one default chat)"] -->|same wire shape| B["Multi-chat harness
(N chats in a session)"] + A -. looks like today .-> A + B -. tabs / tree of chats .-> B +``` + +## 8. Scenarios — and how each harness supports them + +Each scenario below pairs a conceptual **diagram and real-world example** (the +*feature* shape, not the wire format — written in Mermaid so they render in +GitHub, VS Code, and most slide tools) with **how each harness supports it +today**. + +> **Doc-verified snapshot, still a moving target.** The mappings below were +> checked against each product's official documentation (June 2026) and cite +> their sources, but harness capabilities change quickly and several features are +> experimental. The point is the **pattern** — how each product's parallel-agent +> work maps onto the feature — not a permanent scorecard. + +Because the feature degrades gracefully (see §7), no harness ever "breaks" — the +only question is **how many of the scenarios it lights up, and how a UI shows +them.** + +Under each scenario, an **"Across harnesses today"** breakdown covers every +harness across **both surfaces — CLI and desktop app** — with a real-time +example and how it presents in the UI. Verified against official docs (checked +June 2026; see [Sources](#sources)). Several features are experimental and +capabilities move fast, so treat this as a current snapshot. + +**Surface note up front:** Both **Claude Code** and **Codex** ship a graphical +desktop **app** alongside their CLI, and the two apps are strikingly similar: a +left **sidebar of parallel sessions/threads** grouped by the **repo/folder you +open — not by worktree**. Each session still gets its own worktree, but the +sidebar lists **every session in the repo across all features** as a **flat +list**, so unrelated features are interleaved with no per-feature group (the +CLI's "`cd` into a feature worktree to scope its chats" idiom doesn't carry +over). The apps also offer **per-session Git-worktree isolation**, a **split +view** to see two sessions at once, and arrangeable **diff / preview / terminal / +subagent panes**. **GitHub Copilot CLI** is *terminal-only* for this work (its +graphical surface is the separate, coarser VS Code coding-agent). Neither app, +though, models the parallel work as **one shared session with a rolled-up status +across peers** — they list independent sessions side by side. + +### 8.1 User-driven parallel work in one context + +The most common everyday case, and it's **driven by the user, not the agent**: a +person deliberately opens several chats over the *same* shared context to push a +piece of work forward. Three things come together here — the chats **share one +scope** (workspace, model, config), they can **stream concurrently**, and they +stay **grouped under one session** in the UI. No agent is spawning anything; the +human is curating the threads. + +> **Real-world example:** A developer is working in a `client-server` monorepo +> that holds both the backend service and the frontend client, with the model and +> the project's MCP servers configured once at the session level. To build a new +> "live notifications" feature end-to-end, they open two chats themselves — one to +> *build the server* (add the WebSocket endpoint and event schema), and one to +> *build the client* (subscribe to the socket and render the notifications). Both +> chats see the same checked-out branch, the same shared types, and the same lint +> config (shared context). They have the server chat scaffold the endpoint while +> they simultaneously work the client chat against the agreed event shape +> (concurrent). The next morning they reopen the one session and both threads — +> server and client — are still grouped together (grouped), instead of hunting +> through unrelated histories to reassemble the feature. + +```mermaid +flowchart TB + subgraph Session["Session — 'Live notifications' (one shared scope)"] + Ctx["Shared context
client-server repo · shared types · model · MCP servers"] + C1["Chat: build server
WebSocket endpoint + event schema
▶ streaming"] + C2["Chat: build client
subscribe + render notifications
▶ streaming"] + Ctx -. shared by .-> C1 + Ctx -. shared by .-> C2 + end +``` + +**Across harnesses today:** + +**Claude Code** +- **CLI:** Supported via *named sessions* over one workspace — `claude -n + auth-refactor` (or `/rename`), switched through the `/resume` picker (a + terminal TUI that lists every session per project, with worktree/all-project + widening via Ctrl+W / Ctrl+A). Caveat: resuming the *same* session in two + terminals interleaves both transcripts, so genuinely parallel threads need a + fork (see 8.2) or separate named sessions. +- **Desktop app:** Supported and graphical — the **Code tab** lists your sessions + in a sidebar and runs several in parallel; for Git repos **each session gets its + own isolated worktree**, and **Cmd-click** opens two sessions side by side + (split view). Sessions group by the project folder you open. +- **Real-time example:** A dev opens `claude -n api` in one terminal and + `claude -n tests` in another over the same repo, curating two independent + threads side by side. +- **UI:** terminal — the `/resume` session-picker list, or several terminal + windows / tmux panes. Desktop — the Code-tab sidebar of parallel sessions + with split view. + +**Codex** +- **CLI:** One main thread, but `/new` starts a fresh conversation in the same + process and `/agent` switches between active threads. +- **Desktop app:** Supported and graphical — the app organizes work by *project* + and runs multiple threads at once, each **Local** (foreground) or in an + isolated Git **Worktree**; the sidebar lists threads per project. The cloud + view at `chatgpt.com/codex` shows queued/active tasks as cards. +- **Real-time example:** In the Codex app a dev opens one project and launches + three threads — a worktree refactor, a test pass, and a docs update — all + visible in the sidebar and streaming in parallel. +- **UI:** graphical app sidebar (per-project thread list) + cloud task cards; + CLI = thread switching via `/agent`. + +**GitHub Copilot CLI** +- **CLI:** Partial — you can run `copilot` in several terminal instances over the + same workspace, and the `/resume` picker switches between saved sessions, but + only **one at a time** (no live in-session thread switching). `copilot --cloud` + lists "run multiple tasks in parallel" as a use case. +- **Desktop app:** N/A for the CLI — terminal-only. +- **Real-time example:** A dev runs `copilot` in two terminal tabs in the same + repo to push two threads forward at once. +- **UI:** terminal — multiple windows, or the `/resume` session picker. + +#### How the UI looks + +**Claude Code — CLI** (the way a user works a feature in parallel today is to +**create a feature-named git worktree and open a session per piece of +work inside it** — Claude groups sessions by directory, so the worktree *is* the +feature): + +```text + $ git worktree add ../live-notifications # the feature + $ cd ../live-notifications + Terminal A │ Terminal B + $ claude -n build-server │ $ claude -n build-client + ● build-server ▶ streaming │ ● build-client ▶ streaming + > add WebSocket endpoint… │ > subscribe + render notifs… + ───────────────── /resume picker ───────────────── + ▾ ~/code/live-notifications ← the feature (worktree) + ├ build-server ▶ in progress + └ build-client ▶ in progress +``` + +> This is the real, idiomatic flow — the worktree directory is how you name a +> feature and keep its parallel chats together. Its one limit: the grouping is a +> *directory*, not a session object, so there's no rolled-up status/title across +> the chats, and the worktree axis is now spent on grouping (you can't also give +> each chat its *own* worktree — the 8.4 case). AHP keeps the same pattern but +> separates the axes: the **`session`** is the feature (shared scope + +> rolled-up status); **`workingDirectory`** (per session, optionally overridden +> per chat) is the filesystem — so you group *and* can still isolate chats. + +**Claude Code — desktop app** (the **Code tab** groups sessions by the +**repo/folder you open — not by worktree**. Each session still gets its *own* +auto-created worktree, but the sidebar lists **every session in the repo across +all features** as one **flat list**, so the CLI's feature-grouping idiom — `cd` +into a feature worktree to scope its chats — doesn't carry over; unrelated +features are interleaved, with no per-feature group and no rolled-up status): + +```text + ┌ Claude — Code tab · repo: myapp ─────────────────┐ + │ Sessions (ALL features in repo) │ build-server │ + │ ● build-server ▶ wt#1 │ ▶ streaming… │ + │ ● build-client ▶ wt#2 │ ── diff ── │ + │ ● fix-login-bug ▶ wt#3 │ + server.ts │ ← other + │ ● bump-deps idle │ │ features + └─────────────────────────────────┴─────────────────┘ + grouped by REPO, not feature · all features mixed · per-session worktrees +``` + +**Codex — CLI** (same idiom — a feature-named worktree holds the parallel +threads; `/agent` switches between them, `/new` opens another): + +```text + $ git worktree add ../live-notifications && cd ../live-notifications + codex › /agent + active threads (in this feature worktree) ─────── + 1 ● build-server ▶ running + 2 ○ build-client idle + switch 1–2 · /new = another thread in this feature +``` + +**Codex — desktop app** (same story — the app groups threads by the **repo/folder +you open, not by worktree**. Open the repo and you see **every thread across all +features** in one **flat list**; the worktree is only a per-thread *run* location, +never a grouping node — so there's no per-feature group and no rolled-up status): + +```text + ┌ Codex app · repo: myapp ────────────────────────┐ + │ Threads (ALL features in repo) │ build-server │ + │ ● build-server ▶ wt#1 │ ▶ streaming… │ + │ ● build-client ▶ wt#2 │ [review pane]│ + │ ● fix-login-bug ▶ wt#3 │ │ ← other + │ ● bump-deps idle │ │ features + └─────────────────────────────────┴───────────────┘ + grouped by REPO, not feature · all features mixed in one list +``` + +**GitHub Copilot CLI — CLI** (same idiom — create the feature worktree, then run +a `copilot` per piece of work; `/resume` reopens one at a time): + +```text + $ git worktree add ../live-notifications && cd ../live-notifications + Terminal 1: $ copilot Terminal 2: $ copilot + > build the server endpoint > build the client subscriber + ● working… ● working… + (desktop app: N/A — terminal-only) +``` + +### 8.2 Forking + +A new chat is forked from a point in an existing chat, seeded with that history, +then diverges on its own. Both chats keep sharing the session's context. + +> **Real-world example:** Mid-debugging, at message 12 the agent proposes two +> fixes for a race condition. Rather than lose the current thread, the developer +> forks a new chat *seeded from message 12* to try approach B (rewrite the path +> around a queue) while the original chat still holds approach A. Both forks +> share the same repo and config; the developer compares the two outcomes and +> keeps the winner. + +```mermaid +flowchart LR + subgraph ChatA["Chat A"] + direction LR + A1["msg 1"] --> A2["msg 2"] --> A3["msg 3"] + end + subgraph ChatB["Chat B — seeded from A @ msg 2"] + direction LR + B1["seed"] --> B2["msg 1"] --> B3["msg 2"] + end + A2 -. fork from here .-> B1 +``` + +**Across harnesses today:** + +**Claude Code** +- **CLI:** Supported — `/branch [name]` copies the conversation so far and + switches you into it (original preserved, resumable via `/resume`); + `claude --continue --fork-session` does the same from the command line; inside + a `/btw` overlay, `f` forks a new session inheriting the parent transcript plus + that Q&A. Distinct from rewind/checkpoints (Esc-Esc), which edit the *same* + thread. +- **Desktop app:** Supported — the Code tab has **side chats** (`Cmd+;`): a side + question that reuses the session's context without derailing the main thread — + effectively a lightweight in-app fork. A **full fork lands as a flat sibling** + session in the repo list (in its own worktree); unlike the CLI's `/resume` tree, + the parent↔fork relationship is **not** shown in the sidebar. +- **Real-time example:** Mid-debug at message 12, the dev runs + `/branch approach-b` to try the alternate fix while the original thread stays + intact. +- **UI:** terminal — forks are grouped under their root session in the `/resume` + picker (expand with `→`). Desktop — a side chat opens beside the session, or a + forked **sibling** session in the flat sidebar (no fork tree). + +**Codex** +- **CLI:** Supported, several ways — `/fork` clones the current conversation into + a new thread (fresh ID, original untouched); pressing **Esc twice then Enter** + forks from an earlier message you walked back to; `/side` (alias `/btw`) opens + an ephemeral side branch while still showing the parent thread's status; + `codex fork` forks a *saved* session from a picker. +- **Desktop app:** Forks surface as **new threads** in the repo's flat thread + list — **always as siblings**, never nested under the parent. Codex differs from + Claude in *where the fork runs*: the new-thread composer lets it stay in the + **same worktree**, take a fresh **Worktree**, or go to the **Cloud** — but + either way the thread is a flat sibling, so the parent↔fork link isn't shown. +- **Real-time example:** The dev presses Esc twice to walk back to message 12 and + hits Enter to fork "approach B"; the original transcript is preserved. +- **UI:** terminal CLI; in the app the fork is a new sibling thread entry (no fork + tree). + +**GitHub Copilot CLI** +- **CLI:** Not supported — there is no fork/branch concept. `/clear` starts fresh + with no seeded history, and `/resume` returns to a past session but can't branch + it. +- **Desktop app:** N/A. +- **Real-time example:** None — the closest workaround is starting a new session + and manually re-establishing context. +- **UI:** N/A. + +#### How the UI looks + +**Claude Code — CLI** (`/branch` copies the conversation and the fork shows up +nested under its root in `/resume`; because both forks then **edit files**, you +typically back each with its own **worktree** so approach-A and approach-B don't +clobber each other): + +```text + $ git worktree add ../approach-b # isolate the fork's edits + claude › /branch approach-b + ✓ copied conversation @ msg 12 → "approach-b" (original intact) + ───────────────── /resume picker ───────────────── + ▾ debug-race-condition + ├ approach-a ▶ (wd: ./) ◀ edits here + └ approach-b ▶ (wd: ../approach-b) ◀ isolated edits +``` + +**Claude Code — desktop app** (unlike the CLI's `/resume` tree, a fork here is +**always created as a flat sibling** session in the repo's session list — it is +*not* nested under its parent, so the parent↔fork relationship is lost in the +sidebar. A `Cmd+;` **side chat** is the only in-place branch that stays attached +to the session's context): + +```text + ┌ Claude — Code tab · repo: myapp ─┬ side chat (Cmd+;) ─┐ + │ Sessions (flat — no fork tree) │ "try approach-b…" │ + │ ● debug-race ▶ msg 12 │ uses session ctx, │ + │ ● approach-b ▶ wt#2 │ main thread intact │ + │ (fork — sibling, NOT nested) │ │ + └──────────────────────────────────┴────────────────────┘ + fork = new sibling session · parent/fork link not shown in sidebar +``` + +**Codex — CLI** (`/fork` clones the thread; Esc-Esc walks back then Enter forks; +`/side` is an ephemeral branch): + +```text + codex › (Esc Esc → walk back to msg 12) … Enter = fork from here + codex › /fork + ✓ cloned thread → new id (original transcript untouched) + /side = ephemeral side branch (parent status still shown) +``` + +**Codex — desktop app** (the new-thread composer picks *where the fork runs* — +same/Local worktree, a fresh isolated **Worktree**, or a **Cloud** task — but the +resulting thread is **always a flat sibling** in the repo list, not nested): + +```text + ┌ New thread ─────────────────────────┐ repo: myapp (flat list) + │ Fork from: debug-race @ msg 12 │ ● debug-race ▶ + │ Run as: (•) Local (same worktree) │ → ● approach-b ▶ ← sibling, + │ ( ) Worktree ← new dir │ (fork, NOT nested) + │ ( ) Cloud task │ + └──────────────────────────────────────┘ +``` + +**GitHub Copilot CLI** — **not supported**; there is no fork/branch. `/clear` +only starts fresh with no seeded history. (Desktop app: N/A.) + +```text + copilot › /clear ✗ starts over — cannot branch from a point +``` + +### 8.3 Mixing models across a plan → build → review pipeline + +Another user-driven case: the developer runs the *same* piece of work through a +sequence of chats, each pinned to a **different model** chosen for what it's good +at — and can keep **iterating on all three in parallel**, because each chat holds +only its own context. The session shares the repo; each chat picks its own model +and keeps its own clean history. + +> **Real-world example:** A developer wants a careful, high-stakes refactor done +> right. In chat 1 they ask a strong reasoning model to *come up with the plan* — +> break the refactor into steps and flag the risky parts. In chat 2 they hand that +> plan to a fast coding-optimized model to *implement it*. In chat 3 they pin a +> third, independent model to *review the implementation* with fresh eyes — no +> attachment to the choices the implementer made. All three chats share the same +> repo and branch; only the model differs per chat, so each stage uses the model +> best suited to it and the review stays genuinely independent. +> +> Because each stage is its own chat, the developer can **iterate on all three in +> parallel without polluting each other's context**: refine the plan in chat 1, +> push a fix in chat 2, and re-run the review in chat 3 — each conversation keeps +> only the history relevant to *its* job. The planning model never has the noisy +> implementation transcript dumped into its context, and the reviewer never +> inherits the implementer's rationalizations, so every stage stays focused and +> the review stays unbiased even as the work goes back and forth. + +```mermaid +flowchart LR + subgraph Session["Session — 'Refactor X' (shared repo + context)"] + direction LR + C1["Chat 1: plan
model: reasoning-strong"] + C2["Chat 2: implement
model: coding-fast"] + C3["Chat 3: review
model: independent"] + C1 -. plan feeds .-> C2 + C2 -. impl feeds .-> C3 + end +``` + +**Across harnesses today:** + +**Claude Code** +- **CLI:** Supported — `/model` sets the model for the current session, and in + Agent Teams each teammate can run a different model ("Use Sonnet for each + teammate"; **Default teammate model** in `/config`). A user-driven three-stage + pipeline is assembled manually as separate sessions, each with its own + `/model`. +- **Desktop app:** Supported — every session has a **model picker** next to the + send button (`Cmd+Shift+I`), changeable mid-session, so each parallel session + can run a different model. +- **Real-time example:** A planning session on Opus, an implementer teammate on + Sonnet, and a third independent session on another model for review — each + pinned via `/model`. +- **UI:** terminal — the `/model` selector and `/config` default-teammate-model + setting. Desktop — the per-session model picker. + +**Codex** +- **CLI:** Supported — `/model` switches mid-session, `--model gpt-5.5` at launch, + and each subagent's TOML can pin its own `model` / `model_reasoning_effort`. +- **Desktop app / IDE:** Supported — a model switcher sits directly under the chat + input, switchable per thread. **Cloud tasks are the gap**: they are pinned to + GPT‑5.3‑Codex with no per-task model choice (hence *partial* overall). +- **Real-time example:** A plan thread on GPT‑5.5, an implement thread on + GPT‑5.3‑Codex, and a review thread on a different model — set via `/model` or + the app's switcher. +- **UI:** CLI `/model`; app/IDE switcher under the input. + +**GitHub Copilot CLI** +- **CLI:** Supported — within one `/fleet` prompt you assign models per subtask + ("*Use GPT‑5.3‑Codex to create… Use Claude Opus 4.5 to analyze…*"), and + `@custom-agent` profiles carry their own pinned model (subagents otherwise + default to a low-cost model). +- **Desktop app:** N/A — terminal-only. +- **Real-time example:** One `/fleet` prompt routes design review to Opus and + code generation to GPT‑5.3‑Codex in the same run. +- **UI:** terminal — model choices are written inline in the prompt. + +#### How the UI looks + +**Claude Code — CLI** (`/model` per session; a default model for teammates in +`/config`. All three stages **share one working directory** — review must see what +build produced — so *no* per-chat worktree here, unlike 8.2/8.4): + +```text + # all in the same feature worktree — shared wd + chat 1 (plan) claude › /model opus (wd: ./) + chat 2 (build) claude › /model sonnet (wd: ./) + chat 3 (review) claude › /model … (wd: ./) ← sees build's edits + /config › Default teammate model: Haiku +``` + +**Codex — CLI** (`/model` mid-session; per-subagent model in TOML). **Desktop +app:** a model switcher sits under the chat input, per thread. **Cloud tasks are +the gap** — pinned to GPT‑5.3‑Codex: + +```text + codex › /model gpt-5.5 (plan thread) + codex › /model gpt-5.3-codex (build thread) + app: ⌄ model switcher under composer · cloud task = gpt-5.3-codex (fixed) +``` + +**GitHub Copilot CLI** (models assigned per subtask inside one `/fleet` prompt; +desktop app: N/A): + +```text + copilot › /fleet "…Use gpt-5.3-codex to create… Use Opus 4.5 to review…" + ├─ subagent A · model: gpt-5.3-codex ▶ + └─ subagent B · model: opus-4.5 ▶ +``` + +### 8.4 Task decomposition — an agent team + +Where 8.1 was *user-driven*, this case is **agent-driven**: the harness itself +spins up chats to parallelize work. The key insight is **two layers** — the +harness runs the agents and routes work between them internally; the feature only +*represents* each agent as a chat the user can watch and step into. + +> **Real-world example:** A product manager files "migrate auth from server +> sessions to JWT." The lead agent decomposes it and spins up three workers: +> worker 1 rewrites the backend middleware (worktree A), worker 2 migrates the +> client SDK (worktree B), and worker 3 writes the migration guide. The developer +> watches all three stream in parallel and unblocks worker 2 when it asks which +> token-refresh strategy to use — never touching the other two. + +```mermaid +flowchart TB + subgraph Harness["Agent harness — internal, NOT in the protocol"] + direction TB + Lead["Lead agent"] + W1["Worker 1"] + W2["Worker 2"] + Lead -->|task| W1 + Lead -->|task| W2 + W1 -->|result| Lead + W2 -->|result| Lead + end + + subgraph Feature["What the feature represents — visible to the UI"] + direction TB + S["Session"] + LC["Lead chat (default)"] + WC1["Worker 1 chat
wd: /wt/feature-a"] + WC2["Worker 2 chat
wd: /wt/feature-b"] + S --> LC + S --> WC1 + S --> WC2 + end + + Lead -. surfaced as .-> LC + W1 -. surfaced as .-> WC1 + W2 -. surfaced as .-> WC2 +``` + +Note how the **task/result arrows live entirely inside the harness box** — they +never cross into the feature layer. That is precisely why an agent team needs no +chat-to-chat communication in the protocol. + +**Across harnesses today:** + +**Claude Code** — *the richest case, and the motivating one for this feature.* +- **CLI:** Fully supported via **Agent Teams**: a lead spawns teammates (each a + full Claude Code instance with its own context), coordinated through a shared + **task list** (pending/in-progress/completed, dependencies, file-locked + claiming) and a **mailbox** for direct agent-to-agent messaging, plus + per-teammate models, a plan-approval handshake, and graceful shutdown. + **Experimental** (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`, v2.1.32+). +- **Desktop app:** **Agent Teams is not supported** — the experimental peer-team + feature (lead + teammates, shared task list, mailbox) is **CLI-only**. The Code + tab does have `tasks` and `subagent` panes for ordinary subagent/plan activity, + but it does not run or render an agent team. In the terminal the closest + "dashboard" is **split-pane mode** (tmux / iTerm2, each teammate its own pane) + or **in-process mode** (Shift+Down to cycle, Ctrl+T for the task list). +- **Real-time example:** "Refactor the billing module" → the lead spawns an API + teammate and a tests teammate **in the CLI**, each in its own context; all three + run in parallel and one pauses for a plan-approval decision while the dev cycles + through them with Shift+Down. +- **UI:** terminal only for teams — Shift+Down paging or tmux split panes, plus + the Ctrl+T task list (no agent-team view in the desktop app). + +**Codex** +- **CLI:** Partial — Codex parallelizes through **orchestrated subagent + fan-out** (built-in `default` / `worker` / `explorer` roles plus custom TOML + agents, `max_threads` defaulting to 6), but the parent orchestrates and + collects results; there is **no peer-to-peer messaging or shared task list**, + so this collapses into 8.5 rather than a true team. +- **Desktop app:** **No agent-team support** — the subagent fan-out is a + CLI/config capability; the desktop app organizes independent **threads**, not a + lead-plus-teammates team, and surfaces none of the team coordination. +- **Real-time example:** A main thread spawns a `worker` and an `explorer` + subagent in parallel **from the CLI**; they finish and report back, never + talking to each other. +- **UI:** CLI — subagent activity shown inline; not a peer team, and not an + agent-team view in the app. + +**GitHub Copilot CLI** +- **CLI:** Partial — `/fleet` makes the main agent an **orchestrator** that breaks + a plan into independent subtasks and runs them as parallel subagents with + dependency management; results report back (no peer messaging), so again this + is fan-out, not a peer team. +- **Desktop app:** N/A — terminal-only. +- **Real-time example:** In plan mode the dev picks "Accept plan and build on + autopilot + /fleet," and the orchestrator fans out tests / module-refactor / + docs subagents in parallel. +- **UI:** terminal — subagent progress in the CLI response timeline. + +#### How the UI looks + +**Claude Code — CLI, in-process mode** (Shift+Down cycles teammates; Ctrl+T +toggles the shared task list): + +```text + ● lead — "refactor billing" [Ctrl+T task list] + Shift+Down ▾ cycle teammates + ├ ▶ api-teammate rewriting endpoints + └ ⏸ tests-teammate waiting on plan approval + ── task list ── pending │ in-progress │ done (file-locked claim) +``` + +**Claude Code — CLI, split-pane mode** (tmux / iTerm2 — the closest thing to a +"dashboard," but still terminal panes; each teammate that writes concurrently +runs in its **own worktree** so parallel edits stay isolated): + +```text + ┌ lead ──────────┬ api-teammate ───┐ + │ assigns tasks │ ▶ writing API │ wd: ../wt/api + ├────────────────┼─────────────────┤ + │ task list ✓✓▶ │ tests-teammate │ wd: ../wt/tests + │ │ ⏸ plan approval │ + └────────────────┴─────────────────┘ tmux / iTerm2 panes + each writing teammate = its own worktree (isolated working dir) +``` + +**Claude Code — desktop app** (**Agent Teams is not available here** — the Code +tab runs independent sessions with `tasks` / `subagent` panes, but the +lead-plus-teammates team only exists in the CLI; to run a team you stay in the +terminal): + +```text + ┌ Claude — Code tab ────────────┬ tasks ───────────┐ + │ ● refactor-billing ▶ │ ✓ middleware │ + │ (ordinary session + │ ▶ client SDK │ + │ subagent/plan panes) │ ⏸ migration guide│ + │ ✗ no lead/teammate agent team │ │ + └───────────────────────────────┴──────────────────┘ + agent teams = CLI only · the app shows sessions, not a team +``` + +**Codex — CLI** (orchestrated fan-out — parent spawns subagents, no peer +messaging): + +```text + codex › spawn worker + explorer + ├ worker ▶ implement middleware + └ explorer ▶ read existing auth flow + (parent collects results; agents don't talk to each other) +``` + +**Codex — desktop app** (the sidebar groups by the **repo/folder** — threads are +a **flat list of siblings**, not nested by worktree; each thread can *run in* its +own isolated Git **worktree**, but that's a per-thread isolation attribute, not a +grouping axis): + +```text + ┌ Codex app ───────────────────────────────────┐ + │ Project: billing │ worker · run: wt/feat-a│ + │ ● worker ▶ │ ▶ editing files │ + │ ● explorer ▶ │ ── git diff ── │ + │ (flat siblings) │ + middleware.ts │ + └──────────────────────┴──────────────────────────┘ + worktree = per-thread isolation (a run attribute), not a sidebar group +``` + +**GitHub Copilot CLI** (`/fleet` orchestrator fans the plan out with dependency +management; desktop app: N/A): + +```text + copilot › ⇧⇥ plan → "Accept plan and build on autopilot + /fleet" + orchestrator ▸ fan-out + ├ subagent 1 ▶ backend middleware + ├ subagent 2 ▶ client SDK + └ subagent 3 ▶ migration guide (results report back) +``` + +### 8.5 Agent-driven parallel research (fan-out, then continue) + +A second agent-driven pattern: the main agent doesn't hand off the *whole* job — +it stays in charge, but **dispatches parallel research agents** to investigate +side questions, keeps working on its own thread in the meantime, and folds their +findings back in when they return. Each parallel researcher is surfaced as its +own chat, so the user can watch the research happen alongside the main work. + +> **Real-world example:** The main agent is implementing a caching layer. Rather +> than block, it spins up two research agents in parallel — one to *investigate +> how the codebase currently invalidates caches*, another to *compare Redis vs. +> in-memory trade-offs for this workload*. While they dig, the main agent keeps +> scaffolding the interface. Each researcher streams into its own chat; as each +> returns its summary, the main agent incorporates the answer and proceeds. The +> user sees three live chats — the main implementation plus two short-lived +> research threads — and can peek into a researcher's reasoning without +> interrupting the main work. + +```mermaid +flowchart TB + subgraph Harness["Agent harness — internal, NOT in the protocol"] + direction TB + Main["Main agent
(keeps working)"] + R1["Research agent 1
cache invalidation"] + R2["Research agent 2
Redis vs in-memory"] + Main -->|research request| R1 + Main -->|research request| R2 + R1 -->|findings| Main + R2 -->|findings| Main + end + + subgraph Feature["What the feature represents — visible to the UI"] + direction TB + S["Session"] + MC["Main chat (default)
▶ still streaming"] + RC1["Research chat 1
▶ investigating"] + RC2["Research chat 2
▶ investigating"] + S --> MC + S --> RC1 + S --> RC2 + end + + Main -. surfaced as .-> MC + R1 -. surfaced as .-> RC1 + R2 -. surfaced as .-> RC2 +``` + +The difference from 8.4: there the lead **decomposes and hands off** the work; here +the main agent **stays the driver** and only fans out *research*, continuing its +own thread without blocking. Both are just multiple chats under one session — the +request/findings routing stays inside the harness. + +**Across harnesses today:** + +**Claude Code** +- **CLI:** Supported via **subagents** — the main agent dispatches focused workers + that run in their own context and report results back (lower token cost than a + full team). +- **Desktop app:** Supported — the same **`subagent`** pane that renders team + activity shows the fan-out researchers and folds their findings back into the + session. +- **Real-time example:** While scaffolding a caching layer, the main agent spins + up two subagents — one researching how the codebase invalidates caches, one + comparing Redis vs in-memory — and folds their summaries back in. +- **UI:** terminal — subagents run inline and their findings return to the main + thread. Desktop — the `subagent` pane. + +**Codex** +- **CLI / app:** Supported — the `explorer` subagent role is purpose-built for + read-heavy parallel research, and `spawn_agents_on_csv` fans one agent out per + CSV row for batch investigation. +- **Desktop app:** Subagent/explorer activity is surfaced in the app and CLI. +- **Real-time example:** A main thread dispatches several `explorer` subagents to + investigate different modules in parallel and returns a consolidated answer. +- **UI:** app / CLI. + +**GitHub Copilot CLI** +- **CLI:** Supported — `/fleet` subagents each get their own context window, run + in parallel, and report back to the orchestrator. This is the case `/fleet` fits + most naturally. +- **Desktop app:** N/A — terminal-only. +- **Real-time example:** The orchestrator fans out research subagents to compare + implementation approaches while the main plan proceeds. +- **UI:** terminal — subagent output interleaved in the CLI timeline. + +#### How the UI looks + +**Claude Code — CLI** (subagents run inside the one session and fold results +back; they're **read-heavy research**, so they **share the working directory** — +no isolation worktree needed, unlike the writing forks/teammates in 8.2/8.4. In +the **desktop app** the same activity renders in the `subagent` pane): + +```text + ● main — "caching layer" ▶ scaffolding interface (wd: ./ — shared) + ↳ subagent: cache-invalidation ▶ researching (read-only, same wd) + ↳ subagent: redis-vs-in-memory ▶ researching (read-only, same wd) + findings ⤶ report back into main thread (CLI inline · app: subagent pane) +``` + +**Codex — CLI / desktop app** (the `explorer` role is built for read-heavy +research; `spawn_agents_on_csv` fans one agent out per row). The parallel +explorers appear in the same app thread list shown in §8.1: + +```text + codex › explorer subagents + ├ explorer-1 ▶ how the codebase invalidates caches + └ explorer-2 ▶ redis vs in-memory trade-offs + spawn_agents_on_csv → one agent per CSV row (batch) +``` + +**GitHub Copilot CLI** (`/fleet` research subagents, each its own context window; +desktop app: N/A): + +```text + copilot › /fleet (research fan-out) + ├ subagent ▶ investigate approach A + └ subagent ▶ investigate approach B (report back to orchestrator) +``` + +### 8.6 What this means for the feature + +```mermaid +flowchart LR + subgraph Today["Today: Claude & Codex apps + CLIs; Copilot terminal-only"] + direction TB + TM["Claude Code-tab & Codex app sidebars (flat sessions)
· tmux / iTerm2 panes · interleaved CLI output"] + end + subgraph WithFeature["With multi-chat: any graphical client could show"] + direction TB + S["Session: 'Refactor billing' ▸ NEEDS INPUT"] + S --> L["▶ Lead / orchestrator (default)"] + S --> T1["▶ Chat: API · model: Sonnet"] + S --> T2["⏸ Chat: tests · needs input"] + end + Today -. multi-chat represents the same work as .-> WithFeature +``` + +Two patterns fall out of the scenarios above: + +- **Peer agent teams (8.4) exist only in Claude Code.** Codex and Copilot + parallelize via orchestrated fan-out (subagents that report back, no + agent-to-agent messaging), so for them 8.4 collapses into 8.5. +- **Both Claude Code and Codex ship graphical desktop apps** (Copilot CLI stays + terminal-only), and the two apps look alike — a sidebar of parallel + sessions/threads, per-session Git-worktree isolation, split view, and + diff/preview/subagent panes. But **neither groups parallel work as one shared + *session* with a rolled-up status across peers**: they list independent sessions + as **flat siblings** under the folder/repo. That missing, cross-harness + "session → chats" representation — surface each agent/thread/task as a selectable + chat with its own stream, model, working directory, and a rolled-up session + status — is exactly what multi-chat adds. + +Both apps converge on the same shape today — a project/repo sidebar, an active +session, and a diff/review pane — but each stops short of a rolled-up, +cross-harness *session* of peer chats: + +```text + ┌ Codex app ─────────────────────────────────────────┐ + │ Projects / Threads │ active thread │ + │ ▾ refactor-billing │ ▶ streaming… │ + │ ● api ▶ │ │ + │ ⏸ tests needs in │ ── review pane ── │ + │ ○ docs idle │ diff · run · commit │ + └────────────────────────┴────────────────────────────┘ + no single rolled-up "session" status across the peers +``` + +> Note: the *full* coordination machinery (shared task lists, plan-approval and +> shutdown handshakes, explicit lead/teammate roles, agent-to-agent mailboxes) +> stays **inside each harness** and is a deliberate **future** axis. What +> multi-chat delivers today is the **view + direct-interaction** slice. + +**Sources** (official docs, checked June 2026): + +- Claude Code — Agent Teams: · + Sessions / `/branch` / `--fork-session`: · + Subagents: · + Desktop app (parallel sessions, Git-worktree isolation, split view, side + chats, panes): · + Worktrees: +- Codex — Subagents: · + Cloud (parallel tasks): · + CLI features (`/fork`, `/model`, `--attempts`): · + Desktop app (parallel threads / worktrees): +- GitHub Copilot CLI — `/fleet`: · + CLI command reference: + +--- + +## 9. One-slide summary + +- **Before:** a session *is* one linear chat. +- **After:** a session is a **shared scope**; chats are **streams** over it. +- **Why:** to represent multi-agent / agent-team work honestly in a UI. +- **How agent teams work:** chats represent the agents; the team's coordination + stays inside the harness. +- **What it is not:** not chat-to-chat messaging, not agent hierarchies, not + sub-sessions — those are a deliberate future axis. From 590f67eec2a92f36fab1815e794a287f11ca3e47 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 11 Jun 2026 13:05:14 +0200 Subject: [PATCH 3/3] feat(chat): add interactivity metadata to ChatSummary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `interactivity?: "full" | "read-only" | "hidden"` to `ChatSummary` and `ChatState` to support agent-team patterns where worker chats are read-only (visible for observability) or hidden (internal implementation detail). - "full" — user can send messages and watch (default when absent) - "read-only" — user can watch but not send messages - "hidden" — internal worker not shown in UI Harness sets this based on chat role; UI uses it for controls. Example: Claude Code Agent Teams shows all chats with lead interactive, workers read-only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahptypes/state.generated.go | 18 +++++++++ .../generated/Actions.generated.kt | 11 ++++++ .../generated/State.generated.kt | 23 +++++++++++ clients/rust/crates/ahp-types/src/actions.rs | 10 +++++ clients/rust/crates/ahp-types/src/state.rs | 20 ++++++++++ .../Generated/Actions.generated.swift | 11 ++++++ .../Generated/State.generated.swift | 25 ++++++++++++ schema/actions.schema.json | 21 ++++++++++ schema/commands.schema.json | 12 ++++++ schema/errors.schema.json | 8 ++++ schema/notifications.schema.json | 8 ++++ schema/state.schema.json | 17 +++++++++ scripts/generate-go.ts | 1 + scripts/generate-kotlin.ts | 2 + scripts/generate-rust.ts | 1 + scripts/generate-swift.ts | 2 + types/channels-chat/state.ts | 38 +++++++++++++++++++ 17 files changed, 228 insertions(+) diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 91d13128..7e9f461e 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -771,6 +771,15 @@ type ChatState struct { Agent *AgentSelection `json:"agent,omitempty"` // How this chat came into existence Origin *ChatOrigin `json:"origin,omitempty"` + // How the user can interact with this chat. + // + // - `"full"` — user can send messages and watch (default when absent) + // - `"read-only"` — user can watch but not send messages + // - `"hidden"` — internal worker not shown in UI + // + // Supports agent-team patterns where worker chats are read-only or hidden. + // Absence defaults to `"full"` for backward compatibility. + Interactivity *ChatInteractivity `json:"interactivity,omitempty"` // Optional per-chat working directory. // // If absent, the chat inherits @@ -813,6 +822,15 @@ type ChatSummary struct { Agent *AgentSelection `json:"agent,omitempty"` // How this chat came into existence Origin *ChatOrigin `json:"origin,omitempty"` + // How the user can interact with this chat. + // + // - `"full"` — user can send messages and watch (default when absent) + // - `"read-only"` — user can watch but not send messages + // - `"hidden"` — internal worker not shown in UI + // + // Supports agent-team patterns where worker chats are read-only or hidden. + // Absence defaults to `"full"` for backward compatibility. + Interactivity *ChatInteractivity `json:"interactivity,omitempty"` // Optional per-chat working directory. // // If absent, the chat inherits 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 b20eac2f..9bb2108f 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 @@ -1195,6 +1195,17 @@ data class PartialChatSummary( * How this chat came into existence */ val origin: ChatOrigin? = null, + /** + * How the user can interact with this chat. + * + * - `"full"` — user can send messages and watch (default when absent) + * - `"read-only"` — user can watch but not send messages + * - `"hidden"` — internal worker not shown in UI + * + * Supports agent-team patterns where worker chats are read-only or hidden. + * Absence defaults to `"full"` for backward compatibility. + */ + val interactivity: ChatInteractivity? = null, /** * Optional per-chat working directory. * 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 c4dda2d1..74b9caed 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 @@ -22,6 +22,7 @@ import kotlinx.serialization.json.contentOrNull // ─── Type Aliases ─────────────────────────────────────────────────────────── typealias URI = String +typealias ChatInteractivity = String // ─── StringOrMarkdown ─────────────────────────────────────────────────────── @@ -982,6 +983,17 @@ data class ChatState( * How this chat came into existence */ val origin: ChatOrigin? = null, + /** + * How the user can interact with this chat. + * + * - `"full"` — user can send messages and watch (default when absent) + * - `"read-only"` — user can watch but not send messages + * - `"hidden"` — internal worker not shown in UI + * + * Supports agent-team patterns where worker chats are read-only or hidden. + * Absence defaults to `"full"` for backward compatibility. + */ + val interactivity: ChatInteractivity? = null, /** * Optional per-chat working directory. * @@ -1053,6 +1065,17 @@ data class ChatSummary( * How this chat came into existence */ val origin: ChatOrigin? = null, + /** + * How the user can interact with this chat. + * + * - `"full"` — user can send messages and watch (default when absent) + * - `"read-only"` — user can watch but not send messages + * - `"hidden"` — internal worker not shown in UI + * + * Supports agent-team patterns where worker chats are read-only or hidden. + * Absence defaults to `"full"` for backward compatibility. + */ + val interactivity: ChatInteractivity? = null, /** * Optional per-chat working directory. * diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 9d61e65b..d3811f0a 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1329,6 +1329,16 @@ pub struct PartialChatSummary { /// How this chat came into existence #[serde(default, skip_serializing_if = "Option::is_none")] pub origin: Option, + /// How the user can interact with this chat. + /// + /// - `"full"` — user can send messages and watch (default when absent) + /// - `"read-only"` — user can watch but not send messages + /// - `"hidden"` — internal worker not shown in UI + /// + /// Supports agent-team patterns where worker chats are read-only or hidden. + /// Absence defaults to `"full"` for backward compatibility. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub interactivity: Option, /// Optional per-chat working directory. /// /// If absent, the chat inherits diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 87e46501..e536fca9 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -788,6 +788,16 @@ pub struct ChatState { /// How this chat came into existence #[serde(default, skip_serializing_if = "Option::is_none")] pub origin: Option, + /// How the user can interact with this chat. + /// + /// - `"full"` — user can send messages and watch (default when absent) + /// - `"read-only"` — user can watch but not send messages + /// - `"hidden"` — internal worker not shown in UI + /// + /// Supports agent-team patterns where worker chats are read-only or hidden. + /// Absence defaults to `"full"` for backward compatibility. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub interactivity: Option, /// Optional per-chat working directory. /// /// If absent, the chat inherits @@ -842,6 +852,16 @@ pub struct ChatSummary { /// How this chat came into existence #[serde(default, skip_serializing_if = "Option::is_none")] pub origin: Option, + /// How the user can interact with this chat. + /// + /// - `"full"` — user can send messages and watch (default when absent) + /// - `"read-only"` — user can watch but not send messages + /// - `"hidden"` — internal worker not shown in UI + /// + /// Supports agent-team patterns where worker chats are read-only or hidden. + /// Absence defaults to `"full"` for backward compatibility. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub interactivity: Option, /// Optional per-chat working directory. /// /// If absent, the chat inherits diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index ded019d4..30929e20 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1543,6 +1543,15 @@ public struct PartialChatSummary: Codable, Sendable { public var agent: AgentSelection? /// How this chat came into existence public var origin: ChatOrigin? + /// How the user can interact with this chat. + /// + /// - `"full"` — user can send messages and watch (default when absent) + /// - `"read-only"` — user can watch but not send messages + /// - `"hidden"` — internal worker not shown in UI + /// + /// Supports agent-team patterns where worker chats are read-only or hidden. + /// Absence defaults to `"full"` for backward compatibility. + public var interactivity: ChatInteractivity? /// Optional per-chat working directory. /// /// If absent, the chat inherits @@ -1559,6 +1568,7 @@ public struct PartialChatSummary: Codable, Sendable { model: ModelSelection? = nil, agent: AgentSelection? = nil, origin: ChatOrigin? = nil, + interactivity: ChatInteractivity? = nil, workingDirectory: String? = nil ) { self.resource = resource @@ -1569,6 +1579,7 @@ public struct PartialChatSummary: Codable, Sendable { self.model = model self.agent = agent self.origin = origin + self.interactivity = interactivity self.workingDirectory = workingDirectory } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 94dfcfb7..015139b1 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -6,6 +6,8 @@ import Foundation public typealias URI = String +public typealias ChatInteractivity = String + // MARK: - StringOrMarkdown /// A value that is either a plain string or a markdown-formatted string. @@ -745,6 +747,15 @@ public struct ChatState: Codable, Sendable { public var agent: AgentSelection? /// How this chat came into existence public var origin: ChatOrigin? + /// How the user can interact with this chat. + /// + /// - `"full"` — user can send messages and watch (default when absent) + /// - `"read-only"` — user can watch but not send messages + /// - `"hidden"` — internal worker not shown in UI + /// + /// Supports agent-team patterns where worker chats are read-only or hidden. + /// Absence defaults to `"full"` for backward compatibility. + public var interactivity: ChatInteractivity? /// Optional per-chat working directory. /// /// If absent, the chat inherits @@ -775,6 +786,7 @@ public struct ChatState: Codable, Sendable { case model case agent case origin + case interactivity case workingDirectory case turns case activeTurn @@ -793,6 +805,7 @@ public struct ChatState: Codable, Sendable { model: ModelSelection? = nil, agent: AgentSelection? = nil, origin: ChatOrigin? = nil, + interactivity: ChatInteractivity? = nil, workingDirectory: String? = nil, turns: [Turn], activeTurn: ActiveTurn? = nil, @@ -809,6 +822,7 @@ public struct ChatState: Codable, Sendable { self.model = model self.agent = agent self.origin = origin + self.interactivity = interactivity self.workingDirectory = workingDirectory self.turns = turns self.activeTurn = activeTurn @@ -836,6 +850,15 @@ public struct ChatSummary: Codable, Sendable { public var agent: AgentSelection? /// How this chat came into existence public var origin: ChatOrigin? + /// How the user can interact with this chat. + /// + /// - `"full"` — user can send messages and watch (default when absent) + /// - `"read-only"` — user can watch but not send messages + /// - `"hidden"` — internal worker not shown in UI + /// + /// Supports agent-team patterns where worker chats are read-only or hidden. + /// Absence defaults to `"full"` for backward compatibility. + public var interactivity: ChatInteractivity? /// Optional per-chat working directory. /// /// If absent, the chat inherits @@ -852,6 +875,7 @@ public struct ChatSummary: Codable, Sendable { model: ModelSelection? = nil, agent: AgentSelection? = nil, origin: ChatOrigin? = nil, + interactivity: ChatInteractivity? = nil, workingDirectory: String? = nil ) { self.resource = resource @@ -862,6 +886,7 @@ public struct ChatSummary: Codable, Sendable { self.model = model self.agent = agent self.origin = origin + self.interactivity = interactivity self.workingDirectory = workingDirectory } } diff --git a/schema/actions.schema.json b/schema/actions.schema.json index a1ff1862..6656d5a7 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -237,6 +237,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." @@ -3646,6 +3650,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." @@ -3729,6 +3737,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." @@ -6035,6 +6047,15 @@ } ] }, + "ChatInteractivity": { + "type": "string", + "enum": [ + "full", + "read-only", + "hidden" + ], + "description": "How a user can interact with a chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages (e.g. agent team workers)\n- `\"hidden\"` — internal worker not shown in UI at all\n\nSupports the agent-team pattern where a lead chat is fully interactive and\nworker chats are read-only (visible for observability) or hidden (internal\nimplementation detail). The harness sets this based on the chat's role;\nthe UI uses it to show appropriate controls." + }, "ChatInputQuestion": { "oneOf": [ { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index a3b0099f..b3520dfd 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -3059,6 +3059,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." @@ -3142,6 +3146,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." @@ -5505,6 +5513,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 658b3207..54fb9255 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -1911,6 +1911,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." @@ -1994,6 +1998,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index e845235f..ee5fb9ce 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2040,6 +2040,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." @@ -2123,6 +2127,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." diff --git a/schema/state.schema.json b/schema/state.schema.json index 27057acb..f578bd46 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -1822,6 +1822,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nHosts MAY override this for individual chats — for example, to give a\nsubordinate chat its own git worktree so multiple chats in a session can\nmake independent edits that the orchestrator later merges back." @@ -1905,6 +1909,10 @@ "$ref": "#/$defs/ChatOrigin", "description": "How this chat came into existence" }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages\n- `\"hidden\"` — internal worker not shown in UI\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to `\"full\"` for backward compatibility." + }, "workingDirectory": { "$ref": "#/$defs/URI", "description": "Optional per-chat working directory.\n\nIf absent, the chat inherits\n{@link SessionSummary.workingDirectory | the session's working directory}.\nSee {@link ChatState.workingDirectory} for usage notes." @@ -4211,6 +4219,15 @@ } ] }, + "ChatInteractivity": { + "type": "string", + "enum": [ + "full", + "read-only", + "hidden" + ], + "description": "How a user can interact with a chat.\n\n- `\"full\"` — user can send messages and watch (default when absent)\n- `\"read-only\"` — user can watch but not send messages (e.g. agent team workers)\n- `\"hidden\"` — internal worker not shown in UI at all\n\nSupports the agent-team pattern where a lead chat is fully interactive and\nworker chats are read-only (visible for observability) or hidden (internal\nimplementation detail). The harness sets this based on the chat's role;\nthe UI uses it to show appropriate controls." + }, "ChatInputQuestion": { "oneOf": [ { diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 676030e2..d0139117 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1852,6 +1852,7 @@ function checkExhaustiveness(project: Project): void { 'URI', 'BaseParams', 'StringOrMarkdown', + 'ChatInteractivity', 'ToolCallState', 'StateAction', 'ActionEnvelope', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index c786e3bb..bd98a2a4 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1042,6 +1042,7 @@ function generateStateFile(project: Project): string { lines.push('// ─── Type Aliases ───────────────────────────────────────────────────────────'); lines.push(''); lines.push('typealias URI = String'); + lines.push('typealias ChatInteractivity = String'); lines.push(''); lines.push('// ─── StringOrMarkdown ───────────────────────────────────────────────────────'); @@ -1833,6 +1834,7 @@ function checkExhaustiveness(project: Project): void { 'ActionType', // emitted directly by generateActionsFile(), not via STATE_ENUMS 'ChangesetOperationTargetKind', // discriminator enum embedded in the hand-rolled ChangesetOperationTarget union 'StringOrMarkdown', // generateStringOrMarkdown() + 'ChatInteractivity', // type alias for string union 'ToolCallState', // TOOL_CALL_STATE_UNION discriminated union 'StateAction', // StateAction enum in generateActionsFile() 'ActionEnvelope', // generateDataClassFromInterface() call in generateActionsFile() diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index eb212f76..692d09b3 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1585,6 +1585,7 @@ function checkExhaustiveness(project: Project): void { 'URI', 'BaseParams', 'StringOrMarkdown', + 'ChatInteractivity', 'ToolCallState', 'StateAction', 'ActionEnvelope', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index ed0f6030..f094bda9 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -909,6 +909,7 @@ function generateStateFile(project: Project): string { lines.push('// MARK: - Type Aliases\n'); lines.push('public typealias URI = String\n'); + lines.push('public typealias ChatInteractivity = String\n'); lines.push('// MARK: - StringOrMarkdown\n'); lines.push(generateStringOrMarkdown()); @@ -1745,6 +1746,7 @@ function checkExhaustiveness(project: Project): void { 'URI', // type alias for string 'BaseParams', // marker base interface; flattened into each command params struct 'StringOrMarkdown', // generateStringOrMarkdown() + 'ChatInteractivity', // type alias for string union 'ToolCallState', // TOOL_CALL_STATE_UNION discriminated union 'StateAction', // StateAction enum in generateActionsFile() 'ActionEnvelope', // generateStructFromInterface() call in generateActionsFile() diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 1b93dcd7..9645641c 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -53,6 +53,17 @@ export interface ChatState { agent?: AgentSelection; /** How this chat came into existence */ origin?: ChatOrigin; + /** + * How the user can interact with this chat. + * + * - `"full"` — user can send messages and watch (default when absent) + * - `"read-only"` — user can watch but not send messages + * - `"hidden"` — internal worker not shown in UI + * + * Supports agent-team patterns where worker chats are read-only or hidden. + * Absence defaults to `"full"` for backward compatibility. + */ + interactivity?: ChatInteractivity; /** * Optional per-chat working directory. * @@ -105,6 +116,17 @@ export interface ChatSummary { agent?: AgentSelection; /** How this chat came into existence */ origin?: ChatOrigin; + /** + * How the user can interact with this chat. + * + * - `"full"` — user can send messages and watch (default when absent) + * - `"read-only"` — user can watch but not send messages + * - `"hidden"` — internal worker not shown in UI + * + * Supports agent-team patterns where worker chats are read-only or hidden. + * Absence defaults to `"full"` for backward compatibility. + */ + interactivity?: ChatInteractivity; /** * Optional per-chat working directory. * @@ -126,6 +148,22 @@ export type ChatOrigin = | { kind: ChatOriginKind.Fork; chat: URI; turnId: string } | { kind: ChatOriginKind.Tool; chat: URI; toolCallId: string }; +/** + * How a user can interact with a chat. + * + * - `"full"` — user can send messages and watch (default when absent) + * - `"read-only"` — user can watch but not send messages (e.g. agent team workers) + * - `"hidden"` — internal worker not shown in UI at all + * + * Supports the agent-team pattern where a lead chat is fully interactive and + * worker chats are read-only (visible for observability) or hidden (internal + * implementation detail). The harness sets this based on the chat's role; + * the UI uses it to show appropriate controls. + * + * @category Chat State + */ +export type ChatInteractivity = 'full' | 'read-only' | 'hidden'; + // ─── Pending Message Types ─────────────────────────────────────────────────── /**