From 8524d647c8d0772d490b2ed79a080828471308f2 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 09:59:43 -0700 Subject: [PATCH 01/14] Add chat-owned background work Chats list work that will resume them when it finishes, as a non-exhaustive union of background shells and subagents, independent of turn lifetime. Hosts update it with chat/backgroundWorkSet and chat/backgroundWorkRemoved and mirror it into session chat summaries. --- .../Generated/Actions.generated.cs | 30 +++ .../JsonSerializerContext.generated.cs | 7 + .../Generated/State.generated.cs | 126 +++++++++++ .../Generated/ActionMetadata.generated.cs | 8 + .../dotnet/src/AgentHostProtocol/Reducers.cs | 36 +++ clients/go/ahp/reducers.go | 43 ++++ clients/go/ahptypes/actions.generated.go | 32 +++ clients/go/ahptypes/common.go | 2 + clients/go/ahptypes/state.generated.go | 131 +++++++++++ .../microsoft/agenthostprotocol/Reducers.kt | 35 +++ .../generated/Actions.generated.kt | 30 +++ .../generated/State.generated.kt | 185 ++++++++++++++++ clients/rust/crates/ahp-types/src/actions.rs | 49 ++++- clients/rust/crates/ahp-types/src/state.rs | 147 +++++++++++++ clients/rust/crates/ahp/src/reducers.rs | 43 +++- .../Generated/Actions.generated.swift | 46 ++++ .../Generated/State.generated.swift | 206 +++++++++++++++++ .../Sources/AgentHostProtocol/Reducers.swift | 27 +++ .../20260929-chat-background-work.json | 4 + docs/guide/state-model.md | 23 ++ schema/actions.schema.json | 207 ++++++++++++++++++ schema/commands.schema.json | 201 +++++++++++++++++ schema/errors.schema.json | 201 +++++++++++++++++ schema/notifications.schema.json | 154 +++++++++++++ schema/state.schema.json | 154 +++++++++++++ scripts/generate-csharp.ts | 20 +- scripts/generate-go.ts | 21 +- scripts/generate-kotlin.ts | 16 ++ scripts/generate-rust.ts | 20 +- scripts/generate-swift.ts | 17 ++ types/action-origin.generated.ts | 8 + types/channels-chat/actions.ts | 29 +++ types/channels-chat/reducer.ts | 23 ++ types/channels-chat/state.ts | 94 ++++++++ types/common/actions.ts | 6 + .../277-chat-backgroundworkset-adds.json | 60 +++++ .../278-chat-backgroundworkset-replaces.json | 51 +++++ ...79-chat-backgroundworkremoved-removes.json | 56 +++++ ...280-chat-backgroundworkremoved-absent.json | 24 ++ ...81-session-chatupdated-backgroundwork.json | 62 ++++++ ...ion-chatupdated-clears-backgroundwork.json | 54 +++++ types/version/registry.ts | 2 + 42 files changed, 2677 insertions(+), 13 deletions(-) create mode 100644 docs/.changes/20260929-chat-background-work.json create mode 100644 types/test-cases/reducers/277-chat-backgroundworkset-adds.json create mode 100644 types/test-cases/reducers/278-chat-backgroundworkset-replaces.json create mode 100644 types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json create mode 100644 types/test-cases/reducers/280-chat-backgroundworkremoved-absent.json create mode 100644 types/test-cases/reducers/281-session-chatupdated-backgroundwork.json create mode 100644 types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index aaab8da5e..d1d882be7 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -63,6 +63,10 @@ public enum ActionType ChatTurnResume, [WireValue("chat/activityChanged")] ChatActivityChanged, + [WireValue("chat/backgroundWorkSet")] + ChatBackgroundWorkSet, + [WireValue("chat/backgroundWorkRemoved")] + ChatBackgroundWorkRemoved, [WireValue("chat/changesetsChanged")] ChatChangesetsChanged, [WireValue("chat/workingDirectorySet")] @@ -1705,6 +1709,26 @@ public sealed record ChatActivityChangedAction public string? Activity { get; init; } } +/// Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn +/// state. Hosts mirror the resulting list through `session/chatUpdated`. +public sealed record ChatBackgroundWorkSetAction +{ + public ActionType Type { get; init; } + + /// The complete entry. + public required BackgroundWork Work { get; init; } +} + +/// Removes finished or no-longer-tracked background work; unknown IDs are a no-op. +/// Hosts mirror the resulting list through `session/chatUpdated`. +public sealed record ChatBackgroundWorkRemovedAction +{ + public ActionType Type { get; init; } + + /// The {@link BackgroundWorkBase.id | id} of the entry to remove. + public required string Id { get; init; } +} + /// The {@link Changeset | catalogue of changesets} the agent host advertises /// for this chat changed. Replaces /// {@link ChatState.changesets | `state.changesets`} entirely @@ -2572,6 +2596,10 @@ public sealed record PartialChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; init; } + /// Background work, mirrored from {@link ChatState.backgroundWork}. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BackgroundWork { get; init; } + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ModifiedAt { get; init; } @@ -2683,6 +2711,8 @@ public StateActionConverter() ["chat/error"] = typeof(ChatErrorAction), ["chat/turnResume"] = typeof(ChatTurnResumeAction), ["chat/activityChanged"] = typeof(ChatActivityChangedAction), + ["chat/backgroundWorkSet"] = typeof(ChatBackgroundWorkSetAction), + ["chat/backgroundWorkRemoved"] = typeof(ChatBackgroundWorkRemovedAction), ["chat/changesetsChanged"] = typeof(ChatChangesetsChangedAction), ["chat/workingDirectorySet"] = typeof(ChatWorkingDirectorySetAction), ["chat/workingDirectoryRemoved"] = typeof(ChatWorkingDirectoryRemovedAction), diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 70a281ce7..916cf4095 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -76,6 +76,11 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(AutomationTriggerEventDefinition))] [JsonSerializable(typeof(AutomationTriggerKind))] [JsonSerializable(typeof(AutomationUpdateRequestedAction))] +[JsonSerializable(typeof(BackgroundShellWork))] +[JsonSerializable(typeof(BackgroundSubagentWork))] +[JsonSerializable(typeof(BackgroundWork))] +[JsonSerializable(typeof(BackgroundWorkKind))] +[JsonSerializable(typeof(BackgroundWorkStatus))] [JsonSerializable(typeof(Changeset))] [JsonSerializable(typeof(ChangesetCapabilities))] [JsonSerializable(typeof(ChangesetClearedAction))] @@ -98,6 +103,8 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(ChangesetStatusChangedAction))] [JsonSerializable(typeof(ChangesSummary))] [JsonSerializable(typeof(ChatActivityChangedAction))] +[JsonSerializable(typeof(ChatBackgroundWorkRemovedAction))] +[JsonSerializable(typeof(ChatBackgroundWorkSetAction))] [JsonSerializable(typeof(ChatChangesetsChangedAction))] [JsonSerializable(typeof(ChatDeltaAction))] [JsonSerializable(typeof(ChatDraftChangedAction))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 17ac24a7a..f10203e2e 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -457,6 +457,32 @@ public enum TerminalLifecycleStatus Exited, } +/// Kind of {@link BackgroundWork}. +/// +/// This is a general/typological union (not a lifecycle), so the discriminant is +/// a `*Kind`. +[JsonConverter(typeof(WireEnumConverter))] +public enum BackgroundWorkKind +{ + /// A shell command that continues after its initiating tool call returns. + [WireValue("shell")] + Shell, + /// A subagent running in the background. + [WireValue("subagent")] + Subagent, +} + +/// Activity of background work that has not finished. +[JsonConverter(typeof(WireEnumConverter))] +public enum BackgroundWorkStatus +{ + [WireValue("running")] + Running, + /// Not making progress on its own, for example a shell waiting for input. + [WireValue("idle")] + Idle, +} + /// Discriminant for the {@link McpServerState} union. [JsonConverter(typeof(WireEnumConverter))] public enum McpServerStatus @@ -1139,6 +1165,10 @@ public sealed class ChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; set; } + /// Background work, mirrored from {@link ChatState.backgroundWork}. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BackgroundWork { get; set; } + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public required string ModifiedAt { get; set; } @@ -1160,6 +1190,70 @@ public sealed class ChatSummary public List? WorkingDirectories { get; set; } } +/// A shell command continuing outside its initiating tool call. +public sealed record BackgroundShellWork +{ + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + public required string Id { get; init; } + + /// Human-readable label, such as the command's purpose or the subagent's name. + public required string Label { get; init; } + + /// Current activity of the unfinished work. + public BackgroundWorkStatus Status { get; init; } + + /// ISO 8601 timestamp when the work started. + public required string StartedAt { get; init; } + + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + [JsonPropertyName("_meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Meta { get; init; } + + public BackgroundWorkKind Kind { get; init; } + + /// Command line, displayed as plain text. + public required string Command { get; init; } + + /// Terminal channel carrying this shell's output, when the host provides one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Terminal { get; init; } +} + +/// A subagent running in the background. Its own state lives in its chat. +public sealed record BackgroundSubagentWork +{ + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + public required string Id { get; init; } + + /// Human-readable label, such as the command's purpose or the subagent's name. + public required string Label { get; init; } + + /// Current activity of the unfinished work. + public BackgroundWorkStatus Status { get; init; } + + /// ISO 8601 timestamp when the work started. + public required string StartedAt { get; init; } + + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + [JsonPropertyName("_meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Meta { get; init; } + + public BackgroundWorkKind Kind { get; init; } + + /// The subagent's chat. + public required string Chat { get; init; } +} + /// Full state for a single chat, loaded when a client subscribes to the chat's /// URI. /// @@ -1186,6 +1280,11 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; set; } + /// Work running outside the current turn that will resume this chat when it + /// finishes, such as background shells and subagents. Independent of turn state. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BackgroundWork { get; set; } + /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public required string ModifiedAt { get; set; } @@ -6156,6 +6255,33 @@ public SessionInputRequestConverter() } } +/// Work running outside the current turn that will resume the owning chat when it finishes. +[JsonConverter(typeof(BackgroundWorkConverter))] +public sealed class BackgroundWork : AhpUnion +{ + /// Creates an empty BackgroundWork (no active variant). + public BackgroundWork() { } + + /// Creates a BackgroundWork wrapping the given variant value. + public BackgroundWork(object? value) : base(value) { } +} + +/// System.Text.Json converter for the BackgroundWork discriminated union. +internal sealed class BackgroundWorkConverter : UnionConverter +{ + public BackgroundWorkConverter() + : base( + discriminator: "kind", + variants: new Dictionary + { + ["shell"] = typeof(BackgroundShellWork), + ["subagent"] = typeof(BackgroundSubagentWork), + }, + allowUnknown: true) + { + } +} + /// TerminalLifecycleState is the current lifecycle of a terminal process. [JsonConverter(typeof(TerminalLifecycleStateConverter))] public sealed class TerminalLifecycleState : AhpUnion diff --git a/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs b/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs index 608a83bf9..ff25d25c7 100644 --- a/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol/Generated/ActionMetadata.generated.cs @@ -82,6 +82,12 @@ public static bool TryGetActionType(object action, out ActionType actionType) case ChatActivityChangedAction value: actionType = value.Type; return true; + case ChatBackgroundWorkRemovedAction value: + actionType = value.Type; + return true; + case ChatBackgroundWorkSetAction value: + actionType = value.Type; + return true; case ChatChangesetsChangedAction value: actionType = value.Type; return true; @@ -399,6 +405,8 @@ public static string GetWireName(ActionType actionType) => ActionType.ChangesetOperationStatusChanged => "changeset/operationStatusChanged", ActionType.ChangesetStatusChanged => "changeset/statusChanged", ActionType.ChatActivityChanged => "chat/activityChanged", + ActionType.ChatBackgroundWorkRemoved => "chat/backgroundWorkRemoved", + ActionType.ChatBackgroundWorkSet => "chat/backgroundWorkSet", ActionType.ChatChangesetsChanged => "chat/changesetsChanged", ActionType.ChatDelta => "chat/delta", ActionType.ChatDraftChanged => "chat/draftChanged", diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs index cbf7e3055..218578a53 100644 --- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs +++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs @@ -91,6 +91,14 @@ private static SessionStatus WithInputNeededStatus(SessionStatus status, List work.Value switch + { + BackgroundShellWork v => v.Id, + BackgroundSubagentWork v => v.Id, + JsonElement e when e.TryGetProperty("id", out JsonElement id) => id.GetString() ?? string.Empty, + _ => string.Empty, + }; + private static string SessionInputRequestId(SessionInputRequest req) => req.Value switch { SessionChatInputRequest v => v.Id, @@ -969,6 +977,33 @@ public static ReduceOutcome ApplyToChat(ChatState state, StateAction action) case ChatActivityChangedAction a: state.Activity = a.Activity; return ReduceOutcome.Applied; + case ChatBackgroundWorkSetAction a: + { + string workId = BackgroundWorkId(a.Work); + state.BackgroundWork ??= new List(); + int idx = state.BackgroundWork.FindIndex(w => BackgroundWorkId(w) == workId); + if (idx < 0) + { + state.BackgroundWork.Add(a.Work); + } + else + { + state.BackgroundWork[idx] = a.Work; + } + + return ReduceOutcome.Applied; + } + case ChatBackgroundWorkRemovedAction a: + { + int idx = state.BackgroundWork?.FindIndex(w => BackgroundWorkId(w) == a.Id) ?? -1; + if (idx < 0) + { + return ReduceOutcome.NoOp; + } + + state.BackgroundWork!.RemoveAt(idx); + return ReduceOutcome.Applied; + } case ChatChangesetsChangedAction a: state.Changesets = CopyList(a.Changesets); return ReduceOutcome.Applied; @@ -1833,6 +1868,7 @@ private static ReduceOutcome ApplySessionChatUpdated(SessionState state, Session if (ch.Title is not null) { s.Title = ch.Title; } if (ch.Status is not null) { s.Status = ch.Status.Value; } if (ch.Activity is not null) { s.Activity = ch.Activity; } + if (ch.BackgroundWork is not null) { s.BackgroundWork = ch.BackgroundWork; } if (ch.ModifiedAt is not null) { s.ModifiedAt = ch.ModifiedAt; } if (ch.Origin is not null) { s.Origin = ch.Origin; } if (ch.Interactivity is not null) { s.Interactivity = ch.Interactivity; } diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index a649f1f65..df262f255 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -323,6 +323,16 @@ func sessionInputRequestID(r ahptypes.SessionInputRequest) (string, bool) { return "", false } +func backgroundWorkID(w ahptypes.BackgroundWork) (string, bool) { + switch v := w.Value.(type) { + case *ahptypes.BackgroundShellWork: + return v.Id, true + case *ahptypes.BackgroundSubagentWork: + return v.Id, true + } + return "", false +} + func childCustomizationID(c ahptypes.ChildCustomization) (string, bool) { switch v := c.Value.(type) { case *ahptypes.AgentCustomization: @@ -559,6 +569,36 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R case *ahptypes.ChatActivityChangedAction: state.Activity = a.Activity return ReduceOutcomeApplied + case *ahptypes.ChatBackgroundWorkSetAction: + id, ok := backgroundWorkID(a.Work) + if !ok { + return ReduceOutcomeNoOp + } + if state.BackgroundWork == nil { + work := []ahptypes.BackgroundWork{} + state.BackgroundWork = &work + } + work := *state.BackgroundWork + for i := range work { + if got, ok := backgroundWorkID(work[i]); ok && got == id { + work[i] = a.Work + return ReduceOutcomeApplied + } + } + *state.BackgroundWork = append(work, a.Work) + return ReduceOutcomeApplied + case *ahptypes.ChatBackgroundWorkRemovedAction: + if state.BackgroundWork == nil { + return ReduceOutcomeNoOp + } + work := *state.BackgroundWork + for i := range work { + if got, ok := backgroundWorkID(work[i]); ok && got == a.Id { + *state.BackgroundWork = append(work[:i], work[i+1:]...) + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp case *ahptypes.ChatChangesetsChangedAction: if a.Changesets == nil { state.Changesets = nil @@ -815,6 +855,9 @@ func mergeChatSummaryPartial(summary *ahptypes.ChatSummary, changes ahptypes.Par if changes.Activity != nil { summary.Activity = changes.Activity } + if changes.BackgroundWork != nil { + summary.BackgroundWork = changes.BackgroundWork + } if changes.ModifiedAt != nil { summary.ModifiedAt = *changes.ModifiedAt } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 3a22dae9f..f82e61208 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -44,6 +44,8 @@ const ( ActionTypeChatError ActionType = "chat/error" ActionTypeChatTurnResume ActionType = "chat/turnResume" ActionTypeChatActivityChanged ActionType = "chat/activityChanged" + ActionTypeChatBackgroundWorkSet ActionType = "chat/backgroundWorkSet" + ActionTypeChatBackgroundWorkRemoved ActionType = "chat/backgroundWorkRemoved" ActionTypeChatChangesetsChanged ActionType = "chat/changesetsChanged" ActionTypeChatWorkingDirectorySet ActionType = "chat/workingDirectorySet" ActionTypeChatWorkingDirectoryRemoved ActionType = "chat/workingDirectoryRemoved" @@ -644,6 +646,22 @@ type ChatActivityChangedAction struct { Activity *string `json:"activity,omitempty"` } +// Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn +// state. Hosts mirror the resulting list through `session/chatUpdated`. +type ChatBackgroundWorkSetAction struct { + Type ActionType `json:"type"` + // The complete entry. + Work BackgroundWork `json:"work"` +} + +// Removes finished or no-longer-tracked background work; unknown IDs are a no-op. +// Hosts mirror the resulting list through `session/chatUpdated`. +type ChatBackgroundWorkRemovedAction struct { + Type ActionType `json:"type"` + // The {@link BackgroundWorkBase.id | id} of the entry to remove. + Id string `json:"id"` +} + // The {@link Changeset | catalogue of changesets} the agent host advertises // for this chat changed. Replaces // {@link ChatState.changesets | `state.changesets`} entirely @@ -1742,6 +1760,8 @@ func (*ChatTurnCancelledAction) isStateAction() {} func (*ChatErrorAction) isStateAction() {} func (*ChatTurnResumeAction) isStateAction() {} func (*ChatActivityChangedAction) isStateAction() {} +func (*ChatBackgroundWorkSetAction) isStateAction() {} +func (*ChatBackgroundWorkRemovedAction) isStateAction() {} func (*ChatChangesetsChangedAction) isStateAction() {} func (*SessionTitleChangedAction) isStateAction() {} func (*ChatUsageAction) isStateAction() {} @@ -1986,6 +2006,18 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "chat/backgroundWorkSet": + var value ChatBackgroundWorkSetAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "chat/backgroundWorkRemoved": + var value ChatBackgroundWorkRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value case "chat/changesetsChanged": var value ChatChangesetsChangedAction if err := json.Unmarshal(data, &value); err != nil { diff --git a/clients/go/ahptypes/common.go b/clients/go/ahptypes/common.go index 988c72001..f4e789727 100644 --- a/clients/go/ahptypes/common.go +++ b/clients/go/ahptypes/common.go @@ -79,6 +79,8 @@ type PartialChatSummary struct { Status *SessionStatus `json:"status,omitempty"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` + // Preserve an explicitly empty background work list in a summary update. + BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt *string `json:"modifiedAt,omitempty"` // How this chat came into existence diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 09c2552fd..df6e88253 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -359,6 +359,28 @@ const ( TerminalLifecycleStatusExited TerminalLifecycleStatus = "exited" ) +// Kind of {@link BackgroundWork}. +// +// This is a general/typological union (not a lifecycle), so the discriminant is +// a `*Kind`. +type BackgroundWorkKind string + +const ( + // A shell command that continues after its initiating tool call returns. + BackgroundWorkKindShell BackgroundWorkKind = "shell" + // A subagent running in the background. + BackgroundWorkKindSubagent BackgroundWorkKind = "subagent" +) + +// Activity of background work that has not finished. +type BackgroundWorkStatus string + +const ( + BackgroundWorkStatusRunning BackgroundWorkStatus = "running" + // Not making progress on its own, for example a shell waiting for input. + BackgroundWorkStatusIdle BackgroundWorkStatus = "idle" +) + // Discriminant for the {@link McpServerState} union. type McpServerStatus string @@ -1248,6 +1270,9 @@ type ChatState struct { Status SessionStatus `json:"status"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` + // Work running outside the current turn that will resume this chat when it + // finishes, such as background shells and subagents. Independent of turn state. + BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` // How this chat came into existence @@ -1322,6 +1347,8 @@ type ChatSummary struct { Status SessionStatus `json:"status"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` + // Background work, mirrored from {@link ChatState.backgroundWork}. + BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` // How this chat came into existence @@ -1337,6 +1364,50 @@ type ChatSummary struct { WorkingDirectories []URI `json:"workingDirectories,omitempty"` } +// A shell command continuing outside its initiating tool call. +type BackgroundShellWork struct { + // Identifier of this entry, unique within the owning chat across all kinds. + // The host derives it however it likes (for example from the kind plus the + // agent's own task id); consumers MUST treat it as opaque. It is the key for + // the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + // convention. + Id string `json:"id"` + // Human-readable label, such as the command's purpose or the subagent's name. + Label string `json:"label"` + // Current activity of the unfinished work. + Status BackgroundWorkStatus `json:"status"` + // ISO 8601 timestamp when the work started. + StartedAt string `json:"startedAt"` + // Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Kind BackgroundWorkKind `json:"kind"` + // Command line, displayed as plain text. + Command string `json:"command"` + // Terminal channel carrying this shell's output, when the host provides one. + Terminal *URI `json:"terminal,omitempty"` +} + +// A subagent running in the background. Its own state lives in its chat. +type BackgroundSubagentWork struct { + // Identifier of this entry, unique within the owning chat across all kinds. + // The host derives it however it likes (for example from the kind plus the + // agent's own task id); consumers MUST treat it as opaque. It is the key for + // the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + // convention. + Id string `json:"id"` + // Human-readable label, such as the command's purpose or the subagent's name. + Label string `json:"label"` + // Current activity of the unfinished work. + Status BackgroundWorkStatus `json:"status"` + // ISO 8601 timestamp when the work started. + StartedAt string `json:"startedAt"` + // Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Kind BackgroundWorkKind `json:"kind"` + // The subagent's chat. + Chat URI `json:"chat"` +} + // Immutable selected-text snapshot captured when a side chat is created. // // The host records this exact text when it accepts `createChat`; later changes @@ -5613,6 +5684,66 @@ func (u SessionInputRequest) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// BackgroundWork is work running outside the current turn that will resume the owning chat when it finishes. +type BackgroundWork struct { + Value isBackgroundWork +} + +// isBackgroundWork is the marker interface implemented by every +// concrete variant of BackgroundWork. +type isBackgroundWork interface{ isBackgroundWork() } + +func (*BackgroundShellWork) isBackgroundWork() {} +func (*BackgroundSubagentWork) isBackgroundWork() {} + +// BackgroundWorkUnknown carries an unrecognized BackgroundWork 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 BackgroundWorkUnknown struct { + Raw json.RawMessage +} + +func (*BackgroundWorkUnknown) isBackgroundWork() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *BackgroundWork) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + switch disc { + case "shell": + var value BackgroundShellWork + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "subagent": + var value BackgroundSubagentWork + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + u.Value = &BackgroundWorkUnknown{Raw: raw} + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u BackgroundWork) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*BackgroundWorkUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if u.Value == nil { + return []byte("null"), nil + } + return json.Marshal(u.Value) +} + // SessionOrigin is the durable origin of a session. type SessionOrigin struct { Value isSessionOrigin 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 12cc31c1a..ff4ec35bb 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -253,6 +253,13 @@ private fun customizationId(c: Customization): String? = when (c) { is CustomizationUnknown -> null } +private fun backgroundWorkId(w: BackgroundWork): String? = when (w) { + is BackgroundWorkShell -> w.value.id + is BackgroundWorkSubagent -> w.value.id + // Unknown variants carry an opaque `raw` JSON object — no id to expose. + is BackgroundWorkUnknown -> null +} + private fun sessionInputRequestId(r: SessionInputRequest): String? = when (r) { is SessionInputRequestChatInput -> r.value.id is SessionInputRequestToolConfirmation -> r.value.id @@ -587,6 +594,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat title = c.title ?: prior.title, status = c.status ?: prior.status, activity = c.activity ?: prior.activity, + backgroundWork = c.backgroundWork ?: prior.backgroundWork, modifiedAt = c.modifiedAt ?: prior.modifiedAt, origin = c.origin ?: prior.origin, workingDirectories = c.workingDirectories ?: prior.workingDirectories, @@ -993,6 +1001,33 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when is StateActionChatActivityChanged -> state.copy(activity = action.value.activity) + is StateActionChatBackgroundWorkSet -> { + val work = action.value.work + val id = backgroundWorkId(work) + if (id == null) state else { + val list = state.backgroundWork ?: emptyList() + val idx = list.indexOfFirst { backgroundWorkId(it) == id } + val updated = if (idx < 0) { + list + work + } else { + list.toMutableList().also { it[idx] = work } + } + state.copy(backgroundWork = updated) + } + } + + is StateActionChatBackgroundWorkRemoved -> { + val list = state.backgroundWork + val idx = list?.indexOfFirst { backgroundWorkId(it) == action.value.id } ?: -1 + if (list == null || idx < 0) { + state + } else { + val next = list.toMutableList() + next.removeAt(idx) + state.copy(backgroundWork = next) + } + } + is StateActionChatChangesetsChanged -> state.copy(changesets = action.value.changesets) 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 7e1aa1f6b..6d4e5d312 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 @@ -54,6 +54,8 @@ value class ActionType(val rawValue: String) { val CHAT_ERROR: ActionType = ActionType("chat/error") val CHAT_TURN_RESUME: ActionType = ActionType("chat/turnResume") val CHAT_ACTIVITY_CHANGED: ActionType = ActionType("chat/activityChanged") + val CHAT_BACKGROUND_WORK_SET: ActionType = ActionType("chat/backgroundWorkSet") + val CHAT_BACKGROUND_WORK_REMOVED: ActionType = ActionType("chat/backgroundWorkRemoved") val CHAT_CHANGESETS_CHANGED: ActionType = ActionType("chat/changesetsChanged") val CHAT_WORKING_DIRECTORY_SET: ActionType = ActionType("chat/workingDirectorySet") val CHAT_WORKING_DIRECTORY_REMOVED: ActionType = ActionType("chat/workingDirectoryRemoved") @@ -729,6 +731,24 @@ data class ChatActivityChangedAction( val activity: String? = null ) +@Serializable +data class ChatBackgroundWorkSetAction( + val type: ActionType, + /** + * The complete entry. + */ + val work: BackgroundWork +) + +@Serializable +data class ChatBackgroundWorkRemovedAction( + val type: ActionType, + /** + * The {@link BackgroundWorkBase.id | id} of the entry to remove. + */ + val id: String +) + @Serializable data class ChatChangesetsChangedAction( val type: ActionType, @@ -1572,6 +1592,10 @@ data class PartialChatSummary( * Human-readable description of what the chat is currently doing */ val activity: String? = null, + /** + * Background work, mirrored from {@link ChatState.backgroundWork}. + */ + val backgroundWork: List? = null, /** * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -1634,6 +1658,8 @@ sealed interface StateAction @JvmInline value class StateActionChatError(val value: ChatErrorAction) : StateAction @JvmInline value class StateActionChatTurnResume(val value: ChatTurnResumeAction) : StateAction @JvmInline value class StateActionChatActivityChanged(val value: ChatActivityChangedAction) : StateAction +@JvmInline value class StateActionChatBackgroundWorkSet(val value: ChatBackgroundWorkSetAction) : StateAction +@JvmInline value class StateActionChatBackgroundWorkRemoved(val value: ChatBackgroundWorkRemovedAction) : StateAction @JvmInline value class StateActionChatChangesetsChanged(val value: ChatChangesetsChangedAction) : StateAction @JvmInline value class StateActionSessionTitleChanged(val value: SessionTitleChangedAction) : StateAction @JvmInline value class StateActionChatUsage(val value: ChatUsageAction) : StateAction @@ -1748,6 +1774,8 @@ internal object StateActionSerializer : KSerializer { "chat/error" -> StateActionChatError(input.json.decodeFromJsonElement(ChatErrorAction.serializer(), element)) "chat/turnResume" -> StateActionChatTurnResume(input.json.decodeFromJsonElement(ChatTurnResumeAction.serializer(), element)) "chat/activityChanged" -> StateActionChatActivityChanged(input.json.decodeFromJsonElement(ChatActivityChangedAction.serializer(), element)) + "chat/backgroundWorkSet" -> StateActionChatBackgroundWorkSet(input.json.decodeFromJsonElement(ChatBackgroundWorkSetAction.serializer(), element)) + "chat/backgroundWorkRemoved" -> StateActionChatBackgroundWorkRemoved(input.json.decodeFromJsonElement(ChatBackgroundWorkRemovedAction.serializer(), element)) "chat/changesetsChanged" -> StateActionChatChangesetsChanged(input.json.decodeFromJsonElement(ChatChangesetsChangedAction.serializer(), element)) "session/titleChanged" -> StateActionSessionTitleChanged(input.json.decodeFromJsonElement(SessionTitleChangedAction.serializer(), element)) "chat/usage" -> StateActionChatUsage(input.json.decodeFromJsonElement(ChatUsageAction.serializer(), element)) @@ -1855,6 +1883,8 @@ internal object StateActionSerializer : KSerializer { is StateActionChatError -> output.json.encodeToJsonElement(ChatErrorAction.serializer(), value.value) is StateActionChatTurnResume -> output.json.encodeToJsonElement(ChatTurnResumeAction.serializer(), value.value) is StateActionChatActivityChanged -> output.json.encodeToJsonElement(ChatActivityChangedAction.serializer(), value.value) + is StateActionChatBackgroundWorkSet -> output.json.encodeToJsonElement(ChatBackgroundWorkSetAction.serializer(), value.value) + is StateActionChatBackgroundWorkRemoved -> output.json.encodeToJsonElement(ChatBackgroundWorkRemovedAction.serializer(), value.value) is StateActionChatChangesetsChanged -> output.json.encodeToJsonElement(ChatChangesetsChangedAction.serializer(), value.value) is StateActionSessionTitleChanged -> output.json.encodeToJsonElement(SessionTitleChangedAction.serializer(), value.value) is StateActionChatUsage -> output.json.encodeToJsonElement(ChatUsageAction.serializer(), value.value) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 5fe8af143..6a7ed3350 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 @@ -766,6 +766,62 @@ enum class TerminalLifecycleStatus { EXITED } +/** + * Kind of {@link BackgroundWork}. + * + * This is a general/typological union (not a lifecycle), so the discriminant is + * a `*Kind`. + */ +@Serializable(with = BackgroundWorkKindSerializer::class) +@JvmInline +value class BackgroundWorkKind(val rawValue: String) { + companion object { + /** + * A shell command that continues after its initiating tool call returns. + */ + val SHELL: BackgroundWorkKind = BackgroundWorkKind("shell") + /** + * A subagent running in the background. + */ + val SUBAGENT: BackgroundWorkKind = BackgroundWorkKind("subagent") + } +} + +internal object BackgroundWorkKindSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("BackgroundWorkKind", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: BackgroundWorkKind) { + encoder.encodeString(value.rawValue) + } + override fun deserialize(decoder: Decoder): BackgroundWorkKind = + BackgroundWorkKind(decoder.decodeString()) +} + +/** + * Activity of background work that has not finished. + */ +@Serializable(with = BackgroundWorkStatusSerializer::class) +@JvmInline +value class BackgroundWorkStatus(val rawValue: String) { + companion object { + val RUNNING: BackgroundWorkStatus = BackgroundWorkStatus("running") + /** + * Not making progress on its own, for example a shell waiting for input. + */ + val IDLE: BackgroundWorkStatus = BackgroundWorkStatus("idle") + } +} + +internal object BackgroundWorkStatusSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("BackgroundWorkStatus", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: BackgroundWorkStatus) { + encoder.encodeString(value.rawValue) + } + override fun deserialize(decoder: Decoder): BackgroundWorkStatus = + BackgroundWorkStatus(decoder.decodeString()) +} + /** * Discriminant for the {@link McpServerState} union. */ @@ -1608,6 +1664,11 @@ data class ChatState( * Human-readable description of what the chat is currently doing */ val activity: String? = null, + /** + * Work running outside the current turn that will resume this chat when it + * finishes, such as background shells and subagents. Independent of turn state. + */ + val backgroundWork: List? = null, /** * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -1713,6 +1774,10 @@ data class ChatSummary( * Human-readable description of what the chat is currently doing */ val activity: String? = null, + /** + * Background work, mirrored from {@link ChatState.backgroundWork}. + */ + val backgroundWork: List? = null, /** * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -1925,6 +1990,78 @@ data class SessionActiveClient( val customizations: List? = null ) +@Serializable +data class BackgroundShellWork( + /** + * Identifier of this entry, unique within the owning chat across all kinds. + * The host derives it however it likes (for example from the kind plus the + * agent's own task id); consumers MUST treat it as opaque. It is the key for + * the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + * convention. + */ + val id: String, + /** + * Human-readable label, such as the command's purpose or the subagent's name. + */ + val label: String, + /** + * Current activity of the unfinished work. + */ + val status: BackgroundWorkStatus, + /** + * ISO 8601 timestamp when the work started. + */ + val startedAt: String, + /** + * Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + */ + @SerialName("_meta") + val meta: Map? = null, + val kind: BackgroundWorkKind, + /** + * Command line, displayed as plain text. + */ + val command: String, + /** + * Terminal channel carrying this shell's output, when the host provides one. + */ + val terminal: String? = null +) + +@Serializable +data class BackgroundSubagentWork( + /** + * Identifier of this entry, unique within the owning chat across all kinds. + * The host derives it however it likes (for example from the kind plus the + * agent's own task id); consumers MUST treat it as opaque. It is the key for + * the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + * convention. + */ + val id: String, + /** + * Human-readable label, such as the command's purpose or the subagent's name. + */ + val label: String, + /** + * Current activity of the unfinished work. + */ + val status: BackgroundWorkStatus, + /** + * ISO 8601 timestamp when the work started. + */ + val startedAt: String, + /** + * Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + */ + @SerialName("_meta") + val meta: Map? = null, + val kind: BackgroundWorkKind, + /** + * The subagent's chat. + */ + val chat: String +) + @Serializable data class SessionChatInputRequest( /** @@ -6861,6 +6998,54 @@ internal object SessionInputRequestSerializer : KSerializer output.encodeJsonElement(element) } } +@Serializable(with = BackgroundWorkSerializer::class) +sealed interface BackgroundWork + +@JvmInline +value class BackgroundWorkShell(val value: BackgroundShellWork) : BackgroundWork +@JvmInline +value class BackgroundWorkSubagent(val value: BackgroundSubagentWork) : BackgroundWork +/** + * Forward-compat catch-all for unknown BackgroundWork discriminators. + * + * Older clients may receive newer wire variants they don't recognise; capturing + * the raw `JsonObject` lets such payloads round-trip through the client unchanged. + * Reducers handle this variant conservatively on a per-union basis (typically + * as a no-op, but see `Reducers.kt` for the exact treatment). + */ +@JvmInline +value class BackgroundWorkUnknown(val raw: JsonObject) : BackgroundWork + +internal object BackgroundWorkSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("BackgroundWork") + + override fun deserialize(decoder: Decoder): BackgroundWork { + val input = decoder as? JsonDecoder + ?: error("BackgroundWork can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for BackgroundWork") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: return BackgroundWorkUnknown(obj) + return when (discriminant) { + "shell" -> BackgroundWorkShell(input.json.decodeFromJsonElement(BackgroundShellWork.serializer(), element)) + "subagent" -> BackgroundWorkSubagent(input.json.decodeFromJsonElement(BackgroundSubagentWork.serializer(), element)) + else -> BackgroundWorkUnknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: BackgroundWork) { + val output = encoder as? JsonEncoder + ?: error("BackgroundWork can only be serialized to JSON") + val element: JsonElement = when (value) { + is BackgroundWorkShell -> output.json.encodeToJsonElement(BackgroundShellWork.serializer(), value.value) + is BackgroundWorkSubagent -> output.json.encodeToJsonElement(BackgroundSubagentWork.serializer(), value.value) + is BackgroundWorkUnknown -> value.raw + } + output.encodeJsonElement(element) + } +} @Serializable(with = SessionOriginSerializer::class) sealed interface SessionOrigin diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index f2c9049c1..0e8459b48 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -15,14 +15,14 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::state::{ AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, - Changeset, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, - ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, - ChatSummary, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, - ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, Message, - ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, - SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, - ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, - ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo, + BackgroundWork, Changeset, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, + ChangesetStatus, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, + ChatOrigin, ChatSummary, ConfirmationOption, ContentRef, Customization, + CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, + McpServerState, Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, + SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, + ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, + ToolCallRiskAssessment, ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -55,6 +55,8 @@ pub enum ActionType { ChatError, ChatTurnResume, ChatActivityChanged, + ChatBackgroundWorkSet, + ChatBackgroundWorkRemoved, ChatChangesetsChanged, ChatWorkingDirectorySet, ChatWorkingDirectoryRemoved, @@ -172,6 +174,10 @@ impl serde::Serialize for ActionType { Self::ChatError => serializer.serialize_str("chat/error"), Self::ChatTurnResume => serializer.serialize_str("chat/turnResume"), Self::ChatActivityChanged => serializer.serialize_str("chat/activityChanged"), + Self::ChatBackgroundWorkSet => serializer.serialize_str("chat/backgroundWorkSet"), + Self::ChatBackgroundWorkRemoved => { + serializer.serialize_str("chat/backgroundWorkRemoved") + } Self::ChatChangesetsChanged => serializer.serialize_str("chat/changesetsChanged"), Self::ChatWorkingDirectorySet => serializer.serialize_str("chat/workingDirectorySet"), Self::ChatWorkingDirectoryRemoved => { @@ -337,6 +343,8 @@ impl<'de> serde::Deserialize<'de> for ActionType { "chat/error" => Self::ChatError, "chat/turnResume" => Self::ChatTurnResume, "chat/activityChanged" => Self::ChatActivityChanged, + "chat/backgroundWorkSet" => Self::ChatBackgroundWorkSet, + "chat/backgroundWorkRemoved" => Self::ChatBackgroundWorkRemoved, "chat/changesetsChanged" => Self::ChatChangesetsChanged, "chat/workingDirectorySet" => Self::ChatWorkingDirectorySet, "chat/workingDirectoryRemoved" => Self::ChatWorkingDirectoryRemoved, @@ -1057,6 +1065,24 @@ pub struct ChatActivityChangedAction { pub activity: Option, } +/// Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn +/// state. Hosts mirror the resulting list through `session/chatUpdated`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatBackgroundWorkSetAction { + /// The complete entry. + pub work: BackgroundWork, +} + +/// Removes finished or no-longer-tracked background work; unknown IDs are a no-op. +/// Hosts mirror the resulting list through `session/chatUpdated`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatBackgroundWorkRemovedAction { + /// The {@link BackgroundWorkBase.id | id} of the entry to remove. + pub id: String, +} + /// The {@link Changeset | catalogue of changesets} the agent host advertises /// for this chat changed. Replaces /// {@link ChatState.changesets | `state.changesets`} entirely @@ -2230,6 +2256,9 @@ pub struct PartialChatSummary { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, + /// Background work, mirrored from {@link ChatState.backgroundWork}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub background_work: 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, @@ -2307,6 +2336,10 @@ pub enum StateAction { ChatTurnResume(ChatTurnResumeAction), #[serde(rename = "chat/activityChanged")] ChatActivityChanged(ChatActivityChangedAction), + #[serde(rename = "chat/backgroundWorkSet")] + ChatBackgroundWorkSet(ChatBackgroundWorkSetAction), + #[serde(rename = "chat/backgroundWorkRemoved")] + ChatBackgroundWorkRemoved(ChatBackgroundWorkRemovedAction), #[serde(rename = "chat/changesetsChanged")] ChatChangesetsChanged(ChatChangesetsChangedAction), #[serde(rename = "session/titleChanged")] diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 83025038b..2ce4fc184 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -994,6 +994,84 @@ pub enum TerminalLifecycleStatus { Exited, } +/// Kind of {@link BackgroundWork}. +/// +/// This is a general/typological union (not a lifecycle), so the discriminant is +/// a `*Kind`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum BackgroundWorkKind { + /// A shell command that continues after its initiating tool call returns. + Shell, + /// A subagent running in the background. + Subagent, + /// Unknown raw value from a newer protocol version, preserved verbatim. + Unknown(String), +} + +impl serde::Serialize for BackgroundWorkKind { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Shell => serializer.serialize_str("shell"), + Self::Subagent => serializer.serialize_str("subagent"), + Self::Unknown(value) => serializer.serialize_str(value), + } + } +} + +impl<'de> serde::Deserialize<'de> for BackgroundWorkKind { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw = ::deserialize(deserializer)?; + Ok(match raw.as_str() { + "shell" => Self::Shell, + "subagent" => Self::Subagent, + _ => Self::Unknown(raw), + }) + } +} + +/// Activity of background work that has not finished. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum BackgroundWorkStatus { + Running, + /// Not making progress on its own, for example a shell waiting for input. + Idle, + /// Unknown raw value from a newer protocol version, preserved verbatim. + Unknown(String), +} + +impl serde::Serialize for BackgroundWorkStatus { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Running => serializer.serialize_str("running"), + Self::Idle => serializer.serialize_str("idle"), + Self::Unknown(value) => serializer.serialize_str(value), + } + } +} + +impl<'de> serde::Deserialize<'de> for BackgroundWorkStatus { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw = ::deserialize(deserializer)?; + Ok(match raw.as_str() { + "running" => Self::Running, + "idle" => Self::Idle, + _ => Self::Unknown(raw), + }) + } +} + /// Discriminant for the {@link McpServerState} union. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum McpServerStatus { @@ -1886,6 +1964,10 @@ pub struct ChatState { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, + /// Work running outside the current turn that will resume this chat when it + /// finishes, such as background shells and subagents. Independent of turn state. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) pub modified_at: String, /// How this chat came into existence @@ -1973,6 +2055,9 @@ pub struct ChatSummary { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, + /// Background work, mirrored from {@link ChatState.backgroundWork}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) pub modified_at: String, /// How this chat came into existence @@ -1991,6 +2076,55 @@ pub struct ChatSummary { pub working_directories: Option>, } +/// A shell command continuing outside its initiating tool call. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BackgroundShellWork { + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + pub id: String, + /// Human-readable label, such as the command's purpose or the subagent's name. + pub label: String, + /// Current activity of the unfinished work. + pub status: BackgroundWorkStatus, + /// ISO 8601 timestamp when the work started. + pub started_at: String, + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// Command line, displayed as plain text. + pub command: String, + /// Terminal channel carrying this shell's output, when the host provides one. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub terminal: Option, +} + +/// A subagent running in the background. Its own state lives in its chat. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BackgroundSubagentWork { + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + pub id: String, + /// Human-readable label, such as the command's purpose or the subagent's name. + pub label: String, + /// Current activity of the unfinished work. + pub status: BackgroundWorkStatus, + /// ISO 8601 timestamp when the work started. + pub started_at: String, + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// The subagent's chat. + pub chat: Uri, +} + /// Immutable selected-text snapshot captured when a side chat is created. /// /// The host records this exact text when it accepts `createChat`; later changes @@ -6174,6 +6308,19 @@ pub enum SessionInputRequest { #[serde(untagged)] Unknown(serde_json::Value), } +/// Work running outside the current turn that will resume the owning chat when it finishes. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum BackgroundWork { + #[serde(rename = "shell")] + Shell(BackgroundShellWork), + #[serde(rename = "subagent")] + Subagent(BackgroundSubagentWork), + /// 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), +} /// Durable origin of a session. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index f4a06cbed..193abfcff 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -57,7 +57,7 @@ use ahp_types::actions::{ ChatTurnStartedAction, StateAction, }; use ahp_types::state::{ - ActiveTurn, AnnotationsState, AutomationRunState, AutomationState, ChangesetOperationStatus, + ActiveTurn, AnnotationsState, AutomationRunState, AutomationState, BackgroundWork, ChangesetOperationStatus, ChangesetState, ChangesetStatus, ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, CustomizationEnablement, ErrorResponsePart, InputRequestResponsePart, McpServerCustomization, McpServerStartingState, McpServerState, @@ -483,6 +483,14 @@ fn session_input_request_id(r: &SessionInputRequest) -> Option<&str> { } } +fn background_work_id(w: &BackgroundWork) -> Option<&str> { + match w { + BackgroundWork::Shell(x) => Some(x.id.as_str()), + BackgroundWork::Subagent(x) => Some(x.id.as_str()), + BackgroundWork::Unknown(v) => v.get("id").and_then(serde_json::Value::as_str), + } +} + fn child_id_of(c: &ChildCustomization) -> Option<&str> { match c { ChildCustomization::Agent(x) => Some(x.id.as_str()), @@ -738,6 +746,9 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - if let Some(activity) = &a.changes.activity { chat.activity = Some(activity.clone()); } + if let Some(work) = &a.changes.background_work { + chat.background_work = Some(work.clone()); + } if let Some(modified_at) = &a.changes.modified_at { chat.modified_at = modified_at.clone(); } @@ -1119,6 +1130,34 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu state.activity = a.activity.clone(); ReduceOutcome::Applied } + StateAction::ChatBackgroundWorkSet(a) => { + let Some(action_id) = background_work_id(&a.work) else { + return ReduceOutcome::NoOp; + }; + let list = state.background_work.get_or_insert_with(Vec::new); + if let Some(idx) = list + .iter() + .position(|w| background_work_id(w) == Some(action_id)) + { + list[idx] = a.work.clone(); + } else { + list.push(a.work.clone()); + } + ReduceOutcome::Applied + } + StateAction::ChatBackgroundWorkRemoved(a) => { + let Some(list) = state.background_work.as_mut() else { + return ReduceOutcome::NoOp; + }; + let Some(idx) = list + .iter() + .position(|w| background_work_id(w) == Some(a.id.as_str())) + else { + return ReduceOutcome::NoOp; + }; + list.remove(idx); + ReduceOutcome::Applied + } StateAction::ChatChangesetsChanged(a) => { state.changesets = a.changesets.clone(); ReduceOutcome::Applied @@ -2221,6 +2260,7 @@ mod tests { title: String::new(), status: SessionStatus::Idle.bits(), activity: None, + background_work: None, modified_at: "1970-01-01T00:00:00.000Z".into(), origin: None, interactivity: None, @@ -2378,6 +2418,7 @@ mod tests { title: "c1".into(), status: SessionStatus::Idle.bits(), activity: None, + background_work: None, modified_at: "1970-01-01T00:00:00.000Z".into(), origin: None, interactivity: None, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index ffa0a277d..a65a68285 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -31,6 +31,8 @@ public enum ActionType: Codable, Sendable, Equatable { case chatError case chatTurnResume case chatActivityChanged + case chatBackgroundWorkSet + case chatBackgroundWorkRemoved case chatChangesetsChanged case chatWorkingDirectorySet case chatWorkingDirectoryRemoved @@ -137,6 +139,8 @@ public enum ActionType: Codable, Sendable, Equatable { case "chat/error": self = .chatError case "chat/turnResume": self = .chatTurnResume case "chat/activityChanged": self = .chatActivityChanged + case "chat/backgroundWorkSet": self = .chatBackgroundWorkSet + case "chat/backgroundWorkRemoved": self = .chatBackgroundWorkRemoved case "chat/changesetsChanged": self = .chatChangesetsChanged case "chat/workingDirectorySet": self = .chatWorkingDirectorySet case "chat/workingDirectoryRemoved": self = .chatWorkingDirectoryRemoved @@ -243,6 +247,8 @@ public enum ActionType: Codable, Sendable, Equatable { case .chatError: try container.encode("chat/error") case .chatTurnResume: try container.encode("chat/turnResume") case .chatActivityChanged: try container.encode("chat/activityChanged") + case .chatBackgroundWorkSet: try container.encode("chat/backgroundWorkSet") + case .chatBackgroundWorkRemoved: try container.encode("chat/backgroundWorkRemoved") case .chatChangesetsChanged: try container.encode("chat/changesetsChanged") case .chatWorkingDirectorySet: try container.encode("chat/workingDirectorySet") case .chatWorkingDirectoryRemoved: try container.encode("chat/workingDirectoryRemoved") @@ -1185,6 +1191,34 @@ public struct ChatActivityChangedAction: Codable, Sendable { } } +public struct ChatBackgroundWorkSetAction: Codable, Sendable { + public var type: ActionType + /// The complete entry. + public var work: BackgroundWork + + public init( + type: ActionType, + work: BackgroundWork + ) { + self.type = type + self.work = work + } +} + +public struct ChatBackgroundWorkRemovedAction: Codable, Sendable { + public var type: ActionType + /// The {@link BackgroundWorkBase.id | id} of the entry to remove. + public var id: String + + public init( + type: ActionType, + id: String + ) { + self.type = type + self.id = id + } +} + public struct ChatChangesetsChangedAction: Codable, Sendable { public var type: ActionType /// New catalogue, or `undefined` to clear it. @@ -2406,6 +2440,8 @@ public struct PartialChatSummary: Codable, Sendable { public var status: SessionStatus? /// Human-readable description of what the chat is currently doing public var activity: String? + /// Background work, mirrored from {@link ChatState.backgroundWork}. + public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String? /// How this chat came into existence @@ -2425,6 +2461,7 @@ public struct PartialChatSummary: Codable, Sendable { title: String? = nil, status: SessionStatus? = nil, activity: String? = nil, + backgroundWork: [BackgroundWork]? = nil, modifiedAt: String? = nil, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, @@ -2434,6 +2471,7 @@ public struct PartialChatSummary: Codable, Sendable { self.title = title self.status = status self.activity = activity + self.backgroundWork = backgroundWork self.modifiedAt = modifiedAt self.origin = origin self.interactivity = interactivity @@ -2470,6 +2508,8 @@ public enum StateAction: Codable, Sendable { case chatError(ChatErrorAction) case chatTurnResume(ChatTurnResumeAction) case chatActivityChanged(ChatActivityChangedAction) + case chatBackgroundWorkSet(ChatBackgroundWorkSetAction) + case chatBackgroundWorkRemoved(ChatBackgroundWorkRemovedAction) case chatChangesetsChanged(ChatChangesetsChangedAction) case sessionTitleChanged(SessionTitleChangedAction) case chatUsage(ChatUsageAction) @@ -2606,6 +2646,10 @@ public enum StateAction: Codable, Sendable { self = .chatTurnResume(try ChatTurnResumeAction(from: decoder)) case "chat/activityChanged": self = .chatActivityChanged(try ChatActivityChangedAction(from: decoder)) + case "chat/backgroundWorkSet": + self = .chatBackgroundWorkSet(try ChatBackgroundWorkSetAction(from: decoder)) + case "chat/backgroundWorkRemoved": + self = .chatBackgroundWorkRemoved(try ChatBackgroundWorkRemovedAction(from: decoder)) case "chat/changesetsChanged": self = .chatChangesetsChanged(try ChatChangesetsChangedAction(from: decoder)) case "session/titleChanged": @@ -2786,6 +2830,8 @@ public enum StateAction: Codable, Sendable { case .chatError(let v): try v.encode(to: encoder) case .chatTurnResume(let v): try v.encode(to: encoder) case .chatActivityChanged(let v): try v.encode(to: encoder) + case .chatBackgroundWorkSet(let v): try v.encode(to: encoder) + case .chatBackgroundWorkRemoved(let v): try v.encode(to: encoder) case .chatChangesetsChanged(let v): try v.encode(to: encoder) case .sessionTitleChanged(let v): try v.encode(to: encoder) case .chatUsage(let v): try v.encode(to: encoder) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 9a85aaa6a..2dab5c7d4 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -760,6 +760,66 @@ public enum TerminalLifecycleStatus: String, Codable, Sendable { case exited = "exited" } +/// Kind of {@link BackgroundWork}. +/// +/// This is a general/typological union (not a lifecycle), so the discriminant is +/// a `*Kind`. +public enum BackgroundWorkKind: Codable, Sendable, Equatable { + /// A shell command that continues after its initiating tool call returns. + case shell + /// A subagent running in the background. + case subagent + /// Unknown raw value from a newer protocol version, preserved verbatim. + case unknown(String) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let raw = try container.decode(String.self) + switch raw { + case "shell": self = .shell + case "subagent": self = .subagent + default: self = .unknown(raw) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .shell: try container.encode("shell") + case .subagent: try container.encode("subagent") + case .unknown(let raw): try container.encode(raw) + } + } +} + +/// Activity of background work that has not finished. +public enum BackgroundWorkStatus: Codable, Sendable, Equatable { + case running + /// Not making progress on its own, for example a shell waiting for input. + case idle + /// Unknown raw value from a newer protocol version, preserved verbatim. + case unknown(String) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let raw = try container.decode(String.self) + switch raw { + case "running": self = .running + case "idle": self = .idle + default: self = .unknown(raw) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .running: try container.encode("running") + case .idle: try container.encode("idle") + case .unknown(let raw): try container.encode(raw) + } + } +} + /// Discriminant for the {@link McpServerState} union. public enum McpServerStatus: Codable, Sendable, Equatable { /// Server has been registered but is not yet running. @@ -1631,6 +1691,9 @@ public struct ChatState: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the chat is currently doing public var activity: String? + /// Work running outside the current turn that will resume this chat when it + /// finishes, such as background shells and subagents. Independent of turn state. + public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String /// How this chat came into existence @@ -1697,6 +1760,7 @@ public struct ChatState: Codable, Sendable { case title case status case activity + case backgroundWork case modifiedAt case origin case interactivity @@ -1716,6 +1780,7 @@ public struct ChatState: Codable, Sendable { title: String, status: SessionStatus, activity: String? = nil, + backgroundWork: [BackgroundWork]? = nil, modifiedAt: String, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, @@ -1733,6 +1798,7 @@ public struct ChatState: Codable, Sendable { self.title = title self.status = status self.activity = activity + self.backgroundWork = backgroundWork self.modifiedAt = modifiedAt self.origin = origin self.interactivity = interactivity @@ -1757,6 +1823,8 @@ public struct ChatSummary: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the chat is currently doing public var activity: String? + /// Background work, mirrored from {@link ChatState.backgroundWork}. + public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String /// How this chat came into existence @@ -1776,6 +1844,7 @@ public struct ChatSummary: Codable, Sendable { title: String, status: SessionStatus, activity: String? = nil, + backgroundWork: [BackgroundWork]? = nil, modifiedAt: String, origin: ChatOrigin? = nil, interactivity: ChatInteractivity? = nil, @@ -1785,6 +1854,7 @@ public struct ChatSummary: Codable, Sendable { self.title = title self.status = status self.activity = activity + self.backgroundWork = backgroundWork self.modifiedAt = modifiedAt self.origin = origin self.interactivity = interactivity @@ -2011,6 +2081,107 @@ public struct SessionActiveClient: Codable, Sendable { } } +public struct BackgroundShellWork: Codable, Sendable { + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + public var id: String + /// Human-readable label, such as the command's purpose or the subagent's name. + public var label: String + /// Current activity of the unfinished work. + public var status: BackgroundWorkStatus + /// ISO 8601 timestamp when the work started. + public var startedAt: String + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + public var meta: [String: AnyCodable]? + public var kind: BackgroundWorkKind + /// Command line, displayed as plain text. + public var command: String + /// Terminal channel carrying this shell's output, when the host provides one. + public var terminal: String? + + enum CodingKeys: String, CodingKey { + case id + case label + case status + case startedAt + case meta = "_meta" + case kind + case command + case terminal + } + + public init( + id: String, + label: String, + status: BackgroundWorkStatus, + startedAt: String, + meta: [String: AnyCodable]? = nil, + kind: BackgroundWorkKind, + command: String, + terminal: String? = nil + ) { + self.id = id + self.label = label + self.status = status + self.startedAt = startedAt + self.meta = meta + self.kind = kind + self.command = command + self.terminal = terminal + } +} + +public struct BackgroundSubagentWork: Codable, Sendable { + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + public var id: String + /// Human-readable label, such as the command's purpose or the subagent's name. + public var label: String + /// Current activity of the unfinished work. + public var status: BackgroundWorkStatus + /// ISO 8601 timestamp when the work started. + public var startedAt: String + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + public var meta: [String: AnyCodable]? + public var kind: BackgroundWorkKind + /// The subagent's chat. + public var chat: String + + enum CodingKeys: String, CodingKey { + case id + case label + case status + case startedAt + case meta = "_meta" + case kind + case chat + } + + public init( + id: String, + label: String, + status: BackgroundWorkStatus, + startedAt: String, + meta: [String: AnyCodable]? = nil, + kind: BackgroundWorkKind, + chat: String + ) { + self.id = id + self.label = label + self.status = status + self.startedAt = startedAt + self.meta = meta + self.kind = kind + self.chat = chat + } +} + public struct SessionChatInputRequest: Codable, Sendable { /// Stable key for this entry, unique within the session's /// {@link SessionState.inputNeeded} list. The host derives it however it likes @@ -7696,6 +7867,41 @@ public enum SessionInputRequest: Codable, Sendable { } } } +public enum BackgroundWork: Codable, Sendable { + case shell(BackgroundShellWork) + case subagent(BackgroundSubagentWork) + /// Unknown or future discriminant; the raw payload is preserved + /// and re-encoded verbatim for forward-compatibility. + case unknown(AnyCodable) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + guard let discriminant = try container.decodeIfPresent(String.self, forKey: .discriminant) else { + self = .unknown(try AnyCodable(from: decoder)) + return + } + switch discriminant { + case "shell": + self = .shell(try BackgroundShellWork(from: decoder)) + case "subagent": + self = .subagent(try BackgroundSubagentWork(from: decoder)) + default: + self = .unknown(try AnyCodable(from: decoder)) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .shell(let value): try value.encode(to: encoder) + case .subagent(let value): try value.encode(to: encoder) + case .unknown(let value): try value.encode(to: encoder) + } + } +} public enum SessionOrigin: Codable, Sendable { case automation(AutomationSessionOrigin) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 18ca76e3a..0ae9349a3 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -73,6 +73,14 @@ private func refineToolCallContributor(_ current: ToolCallContributor?, _ next: } /// Extracts the stable `id` of a session input request, or `nil` for unknown variants. +private func backgroundWorkID(_ w: BackgroundWork) -> String? { + switch w { + case .shell(let x): return x.id + case .subagent(let x): return x.id + case .unknown: return nil + } +} + private func sessionInputRequestID(_ r: SessionInputRequest) -> String? { switch r { case .chatInput(let x): return x.id @@ -204,6 +212,24 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { next.activity = a.activity return next + case .chatBackgroundWorkSet(let a): + guard let id = backgroundWorkID(a.work) else { return state } + var next = state + var work = state.backgroundWork ?? [] + if let idx = work.firstIndex(where: { backgroundWorkID($0) == id }) { + work[idx] = a.work + } else { + work.append(a.work) + } + next.backgroundWork = work + return next + + case .chatBackgroundWorkRemoved(let a): + guard let idx = state.backgroundWork?.firstIndex(where: { backgroundWorkID($0) == a.id }) else { return state } + var next = state + next.backgroundWork?.remove(at: idx) + return next + case .chatChangesetsChanged(let a): var next = state next.changesets = a.changesets @@ -1053,6 +1079,7 @@ private func mergeChatSummaryChanges(_ summary: inout ChatSummary, changes: Part 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 work = changes.backgroundWork { summary.backgroundWork = work } if let modifiedAt = changes.modifiedAt { summary.modifiedAt = modifiedAt } if let origin = changes.origin { summary.origin = origin } if let workingDirectories = changes.workingDirectories { summary.workingDirectories = workingDirectories } diff --git a/docs/.changes/20260929-chat-background-work.json b/docs/.changes/20260929-chat-background-work.json new file mode 100644 index 000000000..3a534a88f --- /dev/null +++ b/docs/.changes/20260929-chat-background-work.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Expose chat-owned background work (background shells and subagents) in chat state and session chat summaries, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime." +} diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 8b63fdf12..217225481 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -141,6 +141,29 @@ For example, `(status & SessionStatus.InProgress) !== 0` is true for both `InPro Subscribable on a [Chat Channel](/specification/chat-channel) at `ahp-chat:/`. A session is a catalog of chats (`SessionState.chats`); each chat carries the per-conversation state — the turn history, the active turn and its streaming response parts (including live input requests), tool calls, steering/queued messages, and the user's in-progress draft. A session starts with a default chat (`SessionState.defaultChat`); hosts advertising the `multipleChats` capability let clients open more via `createChat`. +`backgroundWork` lists work running outside the current turn that will resume the +chat when it finishes: background shells, background subagents, and future kinds. +Hosts publish complete entries with `chat/backgroundWorkSet` and remove them with +`chat/backgroundWorkRemoved` when they finish or are no longer tracked. Entry IDs are +opaque, unique within a chat across all kinds, and scoped to that chat. Hosts mirror +the list into `SessionState.chats` through `session/chatUpdated`, so a session +subscriber can discover its chats' background work without reading transcripts. + +Each entry has a `kind`, a `label`, a `status` (running or idle, for example a shell +waiting for input), and a start time. A shell entry adds its plain-text command and, +when the host provides one, a terminal channel for its output. A subagent entry points +to the subagent's own chat instead of repeating its state. The kind set is +non-exhaustive: clients should keep entries of unknown kinds and may render them from +the common fields. Provider-specific details, such as how a shell's lifetime is tied +to its agent, belong in `_meta`. + +The collection survives turn completion, cancellation, steering, and history +truncation; those actions do not establish whether the work has stopped. Hosts must +reconcile the runtime's current inventory after restoring a chat, rather than replaying +historical work as running. A missing collection means no inventory has been +published; an empty collection contains no active work. This metadata does not +provide process controls or a new turn-completion rule. + ```typescript ChatState { // Chat summary fields, inlined directly (mirrored into SessionState.chats) diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 903a4f1c1..0bfa516e2 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -220,6 +220,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -1339,6 +1346,40 @@ "type" ] }, + "ChatBackgroundWorkSetAction": { + "type": "object", + "description": "Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn\nstate. Hosts mirror the resulting list through `session/chatUpdated`.", + "properties": { + "type": { + "const": "chat/backgroundWorkSet" + }, + "work": { + "$ref": "#/$defs/BackgroundWork", + "description": "The complete entry." + } + }, + "required": [ + "type", + "work" + ] + }, + "ChatBackgroundWorkRemovedAction": { + "type": "object", + "description": "Removes finished or no-longer-tracked background work; unknown IDs are a no-op.\nHosts mirror the resulting list through `session/chatUpdated`.", + "properties": { + "type": { + "const": "chat/backgroundWorkRemoved" + }, + "id": { + "type": "string", + "description": "The {@link BackgroundWorkBase.id | id} of the entry to remove." + } + }, + "required": [ + "type", + "id" + ] + }, "ChatChangesetsChangedAction": { "type": "object", "description": "The {@link Changeset | catalogue of changesets} the agent host advertises\nfor this chat changed. Replaces\n{@link ChatState.changesets | `state.changesets`} entirely\n(full-replacement semantics) — set to `undefined` to clear the catalogue.\n\nEntries SHOULD describe Branch, Uncommitted Changes, or other views scoped\nto the chat's effective {@link ChatState.workingDirectories | working\ndirectories}. Clients subscribe to each advertised changeset URI for\nfile-level updates through the existing `changeset/*` action stream.", @@ -2461,6 +2502,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatBackgroundWorkSetAction" + }, + { + "$ref": "#/$defs/ChatBackgroundWorkRemovedAction" + }, { "$ref": "#/$defs/ChatChangesetsChangedAction" }, @@ -4918,6 +4965,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -5008,6 +5062,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -5035,6 +5096,127 @@ "modifiedAt" ] }, + "BackgroundWorkBase": { + "type": "object", + "description": "Fields common to every {@link BackgroundWork} variant.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt" + ] + }, + "BackgroundShellWork": { + "type": "object", + "description": "A shell command continuing outside its initiating tool call.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "shell" + }, + "command": { + "type": "string", + "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal channel carrying this shell's output, when the host provides one." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "command" + ] + }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -8374,6 +8556,17 @@ ], "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, + "BackgroundWork": { + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], + "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + }, "ChatOrigin": { "oneOf": [ { @@ -8878,6 +9071,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatBackgroundWorkSetAction" + }, + { + "$ref": "#/$defs/ChatBackgroundWorkRemovedAction" + }, { "$ref": "#/$defs/ChatChangesetsChangedAction" }, @@ -9125,6 +9324,14 @@ "type": "string", "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, + "BackgroundWorkStatus": { + "enum": [ + "running", + "idle" + ], + "type": "string", + "description": "Activity of background work that has not finished." + }, "TurnState": { "enum": [ "complete", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 30e5ef08c..61be4cedb 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4075,6 +4075,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -4165,6 +4172,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -4192,6 +4206,127 @@ "modifiedAt" ] }, + "BackgroundWorkBase": { + "type": "object", + "description": "Fields common to every {@link BackgroundWork} variant.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt" + ] + }, + "BackgroundShellWork": { + "type": "object", + "description": "A shell command continuing outside its initiating tool call.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "shell" + }, + "command": { + "type": "string", + "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal channel carrying this shell's output, when the host provides one." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "command" + ] + }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -7538,6 +7673,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -8657,6 +8799,40 @@ "type" ] }, + "ChatBackgroundWorkSetAction": { + "type": "object", + "description": "Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn\nstate. Hosts mirror the resulting list through `session/chatUpdated`.", + "properties": { + "type": { + "const": "chat/backgroundWorkSet" + }, + "work": { + "$ref": "#/$defs/BackgroundWork", + "description": "The complete entry." + } + }, + "required": [ + "type", + "work" + ] + }, + "ChatBackgroundWorkRemovedAction": { + "type": "object", + "description": "Removes finished or no-longer-tracked background work; unknown IDs are a no-op.\nHosts mirror the resulting list through `session/chatUpdated`.", + "properties": { + "type": { + "const": "chat/backgroundWorkRemoved" + }, + "id": { + "type": "string", + "description": "The {@link BackgroundWorkBase.id | id} of the entry to remove." + } + }, + "required": [ + "type", + "id" + ] + }, "ChatChangesetsChangedAction": { "type": "object", "description": "The {@link Changeset | catalogue of changesets} the agent host advertises\nfor this chat changed. Replaces\n{@link ChatState.changesets | `state.changesets`} entirely\n(full-replacement semantics) — set to `undefined` to clear the catalogue.\n\nEntries SHOULD describe Branch, Uncommitted Changes, or other views scoped\nto the chat's effective {@link ChatState.workingDirectories | working\ndirectories}. Clients subscribe to each advertised changeset URI for\nfile-level updates through the existing `changeset/*` action stream.", @@ -9871,6 +10047,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatBackgroundWorkSetAction" + }, + { + "$ref": "#/$defs/ChatBackgroundWorkRemovedAction" + }, { "$ref": "#/$defs/ChatChangesetsChangedAction" }, @@ -10492,6 +10674,25 @@ "type": "string", "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, + "BackgroundWork": { + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], + "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + }, + "BackgroundWorkStatus": { + "enum": [ + "running", + "idle" + ], + "type": "string", + "description": "Activity of background work that has not finished." + }, "ChatInputQuestion": { "oneOf": [ { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index a53d17321..a68783ff9 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2504,6 +2504,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -2594,6 +2601,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -2621,6 +2635,127 @@ "modifiedAt" ] }, + "BackgroundWorkBase": { + "type": "object", + "description": "Fields common to every {@link BackgroundWork} variant.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt" + ] + }, + "BackgroundShellWork": { + "type": "object", + "description": "A shell command continuing outside its initiating tool call.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "shell" + }, + "command": { + "type": "string", + "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal channel carrying this shell's output, when the host provides one." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "command" + ] + }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -7740,6 +7875,25 @@ "type": "string", "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, + "BackgroundWork": { + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], + "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + }, + "BackgroundWorkStatus": { + "enum": [ + "running", + "idle" + ], + "type": "string", + "description": "Activity of background work that has not finished." + }, "ChatInputQuestion": { "oneOf": [ { @@ -8316,6 +8470,12 @@ { "$ref": "#/$defs/ChatActivityChangedAction" }, + { + "$ref": "#/$defs/ChatBackgroundWorkSetAction" + }, + { + "$ref": "#/$defs/ChatBackgroundWorkRemovedAction" + }, { "$ref": "#/$defs/ChatChangesetsChangedAction" }, @@ -8751,6 +8911,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -9759,6 +9926,40 @@ "type" ] }, + "ChatBackgroundWorkSetAction": { + "type": "object", + "description": "Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn\nstate. Hosts mirror the resulting list through `session/chatUpdated`.", + "properties": { + "type": { + "const": "chat/backgroundWorkSet" + }, + "work": { + "$ref": "#/$defs/BackgroundWork", + "description": "The complete entry." + } + }, + "required": [ + "type", + "work" + ] + }, + "ChatBackgroundWorkRemovedAction": { + "type": "object", + "description": "Removes finished or no-longer-tracked background work; unknown IDs are a no-op.\nHosts mirror the resulting list through `session/chatUpdated`.", + "properties": { + "type": { + "const": "chat/backgroundWorkRemoved" + }, + "id": { + "type": "string", + "description": "The {@link BackgroundWorkBase.id | id} of the entry to remove." + } + }, + "required": [ + "type", + "id" + ] + }, "ChatChangesetsChangedAction": { "type": "object", "description": "The {@link Changeset | catalogue of changesets} the agent host advertises\nfor this chat changed. Replaces\n{@link ChatState.changesets | `state.changesets`} entirely\n(full-replacement semantics) — set to `undefined` to clear the catalogue.\n\nEntries SHOULD describe Branch, Uncommitted Changes, or other views scoped\nto the chat's effective {@link ChatState.workingDirectories | working\ndirectories}. Clients subscribe to each advertised changeset URI for\nfile-level updates through the existing `changeset/*` action stream.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 529751b68..821628a7c 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2682,6 +2682,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -2772,6 +2779,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -2799,6 +2813,127 @@ "modifiedAt" ] }, + "BackgroundWorkBase": { + "type": "object", + "description": "Fields common to every {@link BackgroundWork} variant.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt" + ] + }, + "BackgroundShellWork": { + "type": "object", + "description": "A shell command continuing outside its initiating tool call.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "shell" + }, + "command": { + "type": "string", + "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal channel carrying this shell's output, when the host provides one." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "command" + ] + }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -6266,6 +6401,25 @@ "type": "string", "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, + "BackgroundWork": { + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], + "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + }, + "BackgroundWorkStatus": { + "enum": [ + "running", + "idle" + ], + "type": "string", + "description": "Activity of background work that has not finished." + }, "ChatInputQuestion": { "oneOf": [ { diff --git a/schema/state.schema.json b/schema/state.schema.json index 3e529a61c..6f4b2819a 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2415,6 +2415,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -2505,6 +2512,13 @@ "type": "string", "description": "Human-readable description of what the chat is currently doing" }, + "backgroundWork": { + "type": "array", + "items": { + "$ref": "#/$defs/BackgroundWork" + }, + "description": "Background work, mirrored from {@link ChatState.backgroundWork}." + }, "modifiedAt": { "type": "string", "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" @@ -2532,6 +2546,127 @@ "modifiedAt" ] }, + "BackgroundWorkBase": { + "type": "object", + "description": "Fields common to every {@link BackgroundWork} variant.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt" + ] + }, + "BackgroundShellWork": { + "type": "object", + "description": "A shell command continuing outside its initiating tool call.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "shell" + }, + "command": { + "type": "string", + "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal channel carrying this shell's output, when the host provides one." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "command" + ] + }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "status": { + "$ref": "#/$defs/BackgroundWorkStatus", + "description": "Current activity of the unfinished work." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat." + } + }, + "required": [ + "id", + "label", + "status", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -5871,6 +6006,17 @@ ], "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, + "BackgroundWork": { + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], + "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + }, "ChatOrigin": { "oneOf": [ { @@ -6271,6 +6417,14 @@ "type": "string", "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, + "BackgroundWorkStatus": { + "enum": [ + "running", + "idle" + ], + "type": "string", + "description": "Activity of background work that has not finished." + }, "TurnState": { "enum": [ "complete", diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index e05386337..9a17b8b43 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -642,6 +642,7 @@ const STATE_ENUMS = [ 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', + 'BackgroundWorkKind', 'BackgroundWorkStatus', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind', @@ -667,6 +668,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin { name: 'ConfigSchema' }, { name: 'PendingMessage' }, { name: 'ChatSummary', mutable: true }, + { name: 'BackgroundShellWork' }, + { name: 'BackgroundSubagentWork' }, { name: 'ChatState', mutable: true }, { name: 'ChatInputOption' }, { name: 'ChatInputTextQuestion' }, @@ -1157,6 +1160,17 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const BACKGROUND_WORK_UNION: UnionConfig = { + name: 'BackgroundWork', + discriminantField: 'kind', + doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', + variants: [ + { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, + { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, + ], + unknown: true, +}; + const TERMINAL_LIFECYCLE_STATE_UNION: UnionConfig = { name: 'TerminalLifecycleState', discriminantField: 'status', @@ -1407,7 +1421,7 @@ function generateStateFile(project: Project): string { CHAT_INPUT_QUESTION_UNION, CHAT_INPUT_ANSWER_VALUE_UNION, CHAT_INPUT_ANSWER_UNION, TOOL_RESULT_CONTENT_UNION, MESSAGE_ATTACHMENT_UNION, CUSTOMIZATION_UNION, CHILD_CUSTOMIZATION_UNION, CUSTOMIZATION_LOAD_STATE_UNION, - MCP_SERVER_STATUS_UNION, TOOL_CALL_CONTRIBUTOR_UNION, SESSION_INPUT_REQUEST_UNION, + MCP_SERVER_STATUS_UNION, TOOL_CALL_CONTRIBUTOR_UNION, SESSION_INPUT_REQUEST_UNION, BACKGROUND_WORK_UNION, TERMINAL_LIFECYCLE_STATE_UNION, SESSION_ORIGIN_UNION, AUTOMATION_TRIGGER_UNION, AUTOMATION_DISABLE_CONDITION_UNION, AUTOMATION_RUN_ORIGIN_UNION, AUTOMATION_RUN_LIFECYCLE_UNION, @@ -1500,6 +1514,8 @@ const ACTION_VARIANTS: { type: string; variantName: string; tsInterface: string { type: 'chat/error', variantName: 'ChatError', tsInterface: 'ChatErrorAction' }, { type: 'chat/turnResume', variantName: 'ChatTurnResume', tsInterface: 'ChatTurnResumeAction' }, { type: 'chat/activityChanged', variantName: 'ChatActivityChanged', tsInterface: 'ChatActivityChangedAction' }, + { type: 'chat/backgroundWorkSet', variantName: 'ChatBackgroundWorkSet', tsInterface: 'ChatBackgroundWorkSetAction' }, + { type: 'chat/backgroundWorkRemoved', variantName: 'ChatBackgroundWorkRemoved', tsInterface: 'ChatBackgroundWorkRemovedAction' }, { type: 'chat/changesetsChanged', variantName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' }, { type: 'chat/workingDirectorySet', variantName: 'ChatWorkingDirectorySet', tsInterface: 'ChatWorkingDirectorySetAction' }, { type: 'chat/workingDirectoryRemoved', variantName: 'ChatWorkingDirectoryRemoved', tsInterface: 'ChatWorkingDirectoryRemovedAction' }, @@ -2582,7 +2598,7 @@ function checkExhaustiveness(project: Project): void { 'SessionOrigin', 'TerminalLifecycleState', 'AutomationTrigger', 'AutomationDisableCondition', 'AutomationRunOrigin', 'AutomationRunLifecycle', - 'SessionInputRequest', 'ToolCallConfirmationState', 'ToolCallRiskAssessment', + 'SessionInputRequest', 'BackgroundWork', 'ToolCallConfirmationState', 'ToolCallRiskAssessment', 'ReconnectResult', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', 'AhpError', 'AhpErrorDetailsMap', 'AhpErrorCode', 'AhpErrorCodeWithData', diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index ca9765f84..861d3bffd 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -361,7 +361,8 @@ function extractProps(iface: InterfaceDeclaration, project: Project): GoProp[] { const presenceSensitiveCollection = (iface.getName() === 'AutomationDefinitionPatch' && (tsName === 'triggers' || tsName === '_meta')) || ((iface.getName() === 'AutomationDefinition' || iface.getName() === 'AutomationDefinitionPatch') - && tsName === 'disableConditions'); + && tsName === 'disableConditions') + || (tsName === 'backgroundWork' && (iface.getName() === 'ChatState' || iface.getName() === 'ChatSummary')); if (optional && !alreadyPointer && (presenceSensitiveCollection || (!goType.startsWith('[]') && !goType.startsWith('map[')))) { goType = `*${goType}`; } @@ -725,6 +726,7 @@ const STATE_ENUMS = [ 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', + 'BackgroundWorkKind', 'BackgroundWorkStatus', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', @@ -758,6 +760,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'ChangesSummary' }, { name: 'ChatState' }, { name: 'ChatSummary' }, + { name: 'BackgroundShellWork' }, + { name: 'BackgroundSubagentWork' }, { name: 'SideChatSelection' }, { name: 'PendingMessage' }, { name: 'ProjectInfo' }, @@ -1128,6 +1132,17 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const BACKGROUND_WORK_UNION: UnionConfig = { + name: 'BackgroundWork', + discriminantField: 'kind', + doc: 'BackgroundWork is work running outside the current turn that will resume the owning chat when it finishes.', + variants: [ + { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, + { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, + ], + unknown: true, +}; + const SESSION_ORIGIN_UNION: UnionConfig = { name: 'SessionOrigin', discriminantField: 'kind', @@ -1510,6 +1525,7 @@ function generateStateFile(project: Project): string { lines.push(generateDiscriminatedUnion(project, TERMINAL_LIFECYCLE_STATE_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_INPUT_REQUEST_UNION)); + lines.push(generateDiscriminatedUnion(project, BACKGROUND_WORK_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION)); lines.push(''); @@ -1562,6 +1578,8 @@ const ACTION_VARIANTS: { { type: 'chat/error', variantName: 'ChatError', tsInterface: 'ChatErrorAction' }, { type: 'chat/turnResume', variantName: 'ChatTurnResume', tsInterface: 'ChatTurnResumeAction' }, { type: 'chat/activityChanged', variantName: 'ChatActivityChanged', tsInterface: 'ChatActivityChangedAction' }, + { type: 'chat/backgroundWorkSet', variantName: 'ChatBackgroundWorkSet', tsInterface: 'ChatBackgroundWorkSetAction' }, + { type: 'chat/backgroundWorkRemoved', variantName: 'ChatBackgroundWorkRemoved', tsInterface: 'ChatBackgroundWorkRemovedAction' }, { type: 'chat/changesetsChanged', variantName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' }, { type: 'session/titleChanged', variantName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, { type: 'chat/usage', variantName: 'ChatUsage', tsInterface: 'ChatUsageAction' }, @@ -2342,6 +2360,7 @@ function checkExhaustiveness(project: Project): void { 'ToolCallRiskAssessment', 'TerminalLifecycleState', 'SessionInputRequest', + 'BackgroundWork', 'ToolCallConfirmationState', 'ReconnectResult', 'SessionOrigin', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 829149509..6c09aa5d9 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -975,6 +975,7 @@ const STATE_ENUMS = [ 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', + 'BackgroundWorkKind', 'BackgroundWorkStatus', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', @@ -990,6 +991,7 @@ const STATE_STRUCTS = [ 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', + 'BackgroundShellWork', 'BackgroundSubagentWork', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionToolAuthenticationRequest', 'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', @@ -1324,6 +1326,16 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const BACKGROUND_WORK_UNION: UnionConfig = { + name: 'BackgroundWork', + discriminantField: 'kind', + variants: [ + { caseName: 'Shell', structName: 'BackgroundShellWork', discriminantValue: 'shell' }, + { caseName: 'Subagent', structName: 'BackgroundSubagentWork', discriminantValue: 'subagent' }, + ], + unknown: true, +}; + const SESSION_ORIGIN_UNION: UnionConfig = { name: 'SessionOrigin', discriminantField: 'kind', @@ -1458,6 +1470,7 @@ function generateStateFile(project: Project): string { lines.push(generateDiscriminatedUnion(project, TERMINAL_LIFECYCLE_STATE_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_INPUT_REQUEST_UNION)); + lines.push(generateDiscriminatedUnion(project, BACKGROUND_WORK_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION)); lines.push(''); @@ -1505,6 +1518,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'chat/error', caseName: 'ChatError', tsInterface: 'ChatErrorAction' }, { type: 'chat/turnResume', caseName: 'ChatTurnResume', tsInterface: 'ChatTurnResumeAction' }, { type: 'chat/activityChanged', caseName: 'ChatActivityChanged', tsInterface: 'ChatActivityChangedAction' }, + { type: 'chat/backgroundWorkSet', caseName: 'ChatBackgroundWorkSet', tsInterface: 'ChatBackgroundWorkSetAction' }, + { type: 'chat/backgroundWorkRemoved', caseName: 'ChatBackgroundWorkRemoved', tsInterface: 'ChatBackgroundWorkRemovedAction' }, { type: 'chat/changesetsChanged', caseName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' }, { type: 'session/titleChanged', caseName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, { type: 'chat/usage', caseName: 'ChatUsage', tsInterface: 'ChatUsageAction' }, @@ -2368,6 +2383,7 @@ function checkExhaustiveness(project: Project): void { 'ToolCallRiskAssessment', // TOOL_CALL_RISK_ASSESSMENT_UNION discriminated union 'TerminalLifecycleState', // TERMINAL_LIFECYCLE_STATE_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'BackgroundWork', // BACKGROUND_WORK_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ChildCustomizationType', // TS subset alias of CustomizationType; consumers reuse CustomizationType 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index a4da54e20..5190771d1 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -763,6 +763,7 @@ const STATE_ENUMS = [ 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', + 'BackgroundWorkKind', 'BackgroundWorkStatus', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', @@ -809,6 +810,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'PendingMessage' }, { name: 'ChatState' }, { name: 'ChatSummary' }, + { name: 'BackgroundShellWork', omitDiscriminants: true }, + { name: 'BackgroundSubagentWork', omitDiscriminants: true }, { name: 'SideChatSelection' }, { name: 'SessionState' }, { name: 'SessionActiveClient' }, @@ -1192,6 +1195,17 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const BACKGROUND_WORK_UNION: UnionConfig = { + name: 'BackgroundWork', + discriminantField: 'kind', + doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', + variants: [ + { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, + { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, + ], + unknown: true, +}; + const SESSION_ORIGIN_UNION: UnionConfig = { name: 'SessionOrigin', discriminantField: 'kind', @@ -1409,6 +1423,7 @@ function generateStateFile(project: Project): string { lines.push(generateDiscriminatedUnion(project, TERMINAL_LIFECYCLE_STATE_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_INPUT_REQUEST_UNION)); + lines.push(generateDiscriminatedUnion(project, BACKGROUND_WORK_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION)); lines.push(''); @@ -1462,6 +1477,8 @@ const ACTION_VARIANTS: { { type: 'chat/error', variantName: 'ChatError', tsInterface: 'ChatErrorAction' }, { type: 'chat/turnResume', variantName: 'ChatTurnResume', tsInterface: 'ChatTurnResumeAction' }, { type: 'chat/activityChanged', variantName: 'ChatActivityChanged', tsInterface: 'ChatActivityChangedAction' }, + { type: 'chat/backgroundWorkSet', variantName: 'ChatBackgroundWorkSet', tsInterface: 'ChatBackgroundWorkSetAction' }, + { type: 'chat/backgroundWorkRemoved', variantName: 'ChatBackgroundWorkRemoved', tsInterface: 'ChatBackgroundWorkRemovedAction' }, { type: 'chat/changesetsChanged', variantName: 'ChatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' }, { type: 'session/titleChanged', variantName: 'SessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, { type: 'chat/usage', variantName: 'ChatUsage', tsInterface: 'ChatUsageAction' }, @@ -1612,7 +1629,7 @@ impl Serialize for ChatErrorAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, BackgroundWork, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); lines.push(''); // ActionType enum @@ -2246,6 +2263,7 @@ function checkExhaustiveness(project: Project): void { 'ToolCallRiskAssessment', // TOOL_CALL_RISK_ASSESSMENT_UNION discriminated union 'TerminalLifecycleState', // TERMINAL_LIFECYCLE_STATE_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'BackgroundWork', // BACKGROUND_WORK_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ReconnectResult', 'SessionOrigin', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 09f1dde1e..2308eb76f 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -680,6 +680,7 @@ const STATE_ENUMS = [ 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', + 'BackgroundWorkKind', 'BackgroundWorkStatus', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', @@ -695,6 +696,7 @@ const STATE_STRUCTS = [ 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', + 'BackgroundShellWork', 'BackgroundSubagentWork', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionToolAuthenticationRequest', 'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', @@ -979,6 +981,17 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { ], }; +const BACKGROUND_WORK_UNION: UnionConfig = { + name: 'BackgroundWork', + discriminantField: 'kind', + // Open union: future protocol versions may add new background work kinds. + allowUnknown: true, + variants: [ + { caseName: 'shell', structName: 'BackgroundShellWork', discriminantValue: 'shell' }, + { caseName: 'subagent', structName: 'BackgroundSubagentWork', discriminantValue: 'subagent' }, + ], +}; + function generateToolResultContentUnion(): string { return `public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) @@ -1354,6 +1367,7 @@ function generateStateFile(project: Project): string { lines.push(generateDiscriminatedUnion(project, TERMINAL_LIFECYCLE_STATE_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_INPUT_REQUEST_UNION)); + lines.push(generateDiscriminatedUnion(project, BACKGROUND_WORK_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION)); lines.push(''); @@ -1402,6 +1416,8 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'chat/error', caseName: 'chatError', tsInterface: 'ChatErrorAction' }, { type: 'chat/turnResume', caseName: 'chatTurnResume', tsInterface: 'ChatTurnResumeAction' }, { type: 'chat/activityChanged', caseName: 'chatActivityChanged', tsInterface: 'ChatActivityChangedAction' }, + { type: 'chat/backgroundWorkSet', caseName: 'chatBackgroundWorkSet', tsInterface: 'ChatBackgroundWorkSetAction' }, + { type: 'chat/backgroundWorkRemoved', caseName: 'chatBackgroundWorkRemoved', tsInterface: 'ChatBackgroundWorkRemovedAction' }, { type: 'chat/changesetsChanged', caseName: 'chatChangesetsChanged', tsInterface: 'ChatChangesetsChangedAction' }, { type: 'session/titleChanged', caseName: 'sessionTitleChanged', tsInterface: 'SessionTitleChangedAction' }, { type: 'chat/usage', caseName: 'chatUsage', tsInterface: 'ChatUsageAction' }, @@ -2387,6 +2403,7 @@ function checkExhaustiveness(project: Project): void { 'ToolCallRiskAssessment', // TOOL_CALL_RISK_ASSESSMENT_UNION discriminated union 'TerminalLifecycleState', // TERMINAL_LIFECYCLE_STATE_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union + 'BackgroundWork', // BACKGROUND_WORK_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() 'PermissionDeniedErrorData', // emitted by generateErrorsFile() diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 05e0728d1..a8b969971 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -53,6 +53,8 @@ import type { ChatErrorAction, ChatTurnResumeAction, ChatActivityChangedAction, + ChatBackgroundWorkSetAction, + ChatBackgroundWorkRemovedAction, ChatChangesetsChangedAction, ChatWorkingDirectorySetAction, ChatWorkingDirectoryRemovedAction, @@ -217,6 +219,8 @@ export type ChatAction = | ChatErrorAction | ChatTurnResumeAction | ChatActivityChangedAction + | ChatBackgroundWorkSetAction + | ChatBackgroundWorkRemovedAction | ChatChangesetsChangedAction | ChatWorkingDirectorySetAction | ChatWorkingDirectoryRemovedAction @@ -267,6 +271,8 @@ export type ServerChatAction = | ChatTurnCompleteAction | ChatErrorAction | ChatActivityChangedAction + | ChatBackgroundWorkSetAction + | ChatBackgroundWorkRemovedAction | ChatChangesetsChangedAction | ChatUsageAction | ChatReasoningAction @@ -473,6 +479,8 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.ChatError]: false, [ActionType.ChatTurnResume]: true, [ActionType.ChatActivityChanged]: false, + [ActionType.ChatBackgroundWorkSet]: false, + [ActionType.ChatBackgroundWorkRemoved]: false, [ActionType.ChatChangesetsChanged]: false, [ActionType.ChatWorkingDirectorySet]: true, [ActionType.ChatWorkingDirectoryRemoved]: true, diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index 2c44877da..b7355bfe7 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -9,6 +9,7 @@ import type { StringOrMarkdown, FileEditCollection, UsageInfo, URI } from '../co import type { Changeset } from '../channels-changeset/state.js'; import type { McpAuthRequirement } from '../channels-session/state.js'; import type { + BackgroundWork, Message, ResponsePart, ToolCallResult, @@ -546,6 +547,32 @@ export interface ChatActivityChangedAction { activity?: string; } +/** + * Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn + * state. Hosts mirror the resulting list through `session/chatUpdated`. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatBackgroundWorkSetAction { + type: ActionType.ChatBackgroundWorkSet; + /** The complete entry. */ + work: BackgroundWork; +} + +/** + * Removes finished or no-longer-tracked background work; unknown IDs are a no-op. + * Hosts mirror the resulting list through `session/chatUpdated`. + * + * @category Chat Actions + * @version 1 + */ +export interface ChatBackgroundWorkRemovedAction { + type: ActionType.ChatBackgroundWorkRemoved; + /** The {@link BackgroundWorkBase.id | id} of the entry to remove. */ + id: string; +} + /** * The {@link Changeset | catalogue of changesets} the agent host advertises * for this chat changed. Replaces @@ -886,6 +913,8 @@ export type ChatAction = | ChatErrorAction | ChatTurnResumeAction | ChatActivityChangedAction + | ChatBackgroundWorkSetAction + | ChatBackgroundWorkRemovedAction | ChatChangesetsChangedAction | ChatWorkingDirectorySetAction | ChatWorkingDirectoryRemovedAction diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts index 50e16e27e..1ae6ce6dd 100644 --- a/types/channels-chat/reducer.ts +++ b/types/channels-chat/reducer.ts @@ -449,6 +449,29 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st case ActionType.ChatActivityChanged: return { ...state, activity: action.activity }; + case ActionType.ChatBackgroundWorkSet: { + const list = state.backgroundWork ?? []; + const idx = list.findIndex(work => work.id === action.work.id); + const next = list.slice(); + if (idx < 0) { + next.push(action.work); + } else { + next[idx] = action.work; + } + return { ...state, backgroundWork: next }; + } + + case ActionType.ChatBackgroundWorkRemoved: { + const list = state.backgroundWork ?? []; + const idx = list.findIndex(work => work.id === action.id); + if (idx < 0) { + return state; + } + const next = list.slice(); + next.splice(idx, 1); + return { ...state, backgroundWork: next }; + } + case ActionType.ChatChangesetsChanged: { const { changesets: _omit, ...stateWithoutChangesets } = state; return action.changesets diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index ae3df0d85..527a4e6b2 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -49,6 +49,11 @@ export interface ChatState { status: SessionStatus; /** Human-readable description of what the chat is currently doing */ activity?: string; + /** + * Work running outside the current turn that will resume this chat when it + * finishes, such as background shells and subagents. Independent of turn state. + */ + backgroundWork?: BackgroundWork[]; /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ modifiedAt: string; /** How this chat came into existence */ @@ -141,6 +146,8 @@ export interface ChatSummary { status: SessionStatus; /** Human-readable description of what the chat is currently doing */ activity?: string; + /** Background work, mirrored from {@link ChatState.backgroundWork}. */ + backgroundWork?: BackgroundWork[]; /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ modifiedAt: string; /** How this chat came into existence */ @@ -160,6 +167,93 @@ export interface ChatSummary { workingDirectories?: URI[]; } +/** + * Kind of {@link BackgroundWork}. + * + * This is a general/typological union (not a lifecycle), so the discriminant is + * a `*Kind`. + * + * @category Background Work + * @nonexhaustive + */ +export const enum BackgroundWorkKind { + /** A shell command that continues after its initiating tool call returns. */ + Shell = 'shell', + /** A subagent running in the background. */ + Subagent = 'subagent', +} + +/** + * Activity of background work that has not finished. + * + * @category Background Work + * @nonexhaustive + */ +export const enum BackgroundWorkStatus { + Running = 'running', + /** Not making progress on its own, for example a shell waiting for input. */ + Idle = 'idle', +} + +/** + * Fields common to every {@link BackgroundWork} variant. + * + * @category Background Work + */ +interface BackgroundWorkBase { + /** + * Identifier of this entry, unique within the owning chat across all kinds. + * The host derives it however it likes (for example from the kind plus the + * agent's own task id); consumers MUST treat it as opaque. It is the key for + * the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + * convention. + */ + id: string; + /** Human-readable label, such as the command's purpose or the subagent's name. */ + label: string; + /** Current activity of the unfinished work. */ + status: BackgroundWorkStatus; + /** ISO 8601 timestamp when the work started. */ + startedAt: string; + /** Provider-specific metadata, such as how a shell's lifetime is tied to its agent. */ + _meta?: Record; +} + +/** + * A shell command continuing outside its initiating tool call. + * + * @category Background Work + */ +export interface BackgroundShellWork extends BackgroundWorkBase { + kind: BackgroundWorkKind.Shell; + /** Command line, displayed as plain text. */ + command: string; + /** Terminal channel carrying this shell's output, when the host provides one. */ + terminal?: URI; +} + +/** + * A subagent running in the background. Its own state lives in its chat. + * + * @category Background Work + */ +export interface BackgroundSubagentWork extends BackgroundWorkBase { + kind: BackgroundWorkKind.Subagent; + /** The subagent's chat. */ + chat: URI; +} + +/** + * Work running outside the current turn that will resume the owning chat when + * it finishes. Clients that don't recognize a `kind` should keep the entry and + * may render it from the common fields. + * + * @category Background Work + */ +export type BackgroundWork = + | BackgroundShellWork + | BackgroundSubagentWork; + /** * Discriminant for {@link ChatOrigin} — how a chat came into existence. * diff --git a/types/common/actions.ts b/types/common/actions.ts index b79ebfb7c..c96010e8f 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -65,6 +65,8 @@ import type { ChatErrorAction, ChatTurnResumeAction, ChatActivityChangedAction, + ChatBackgroundWorkSetAction, + ChatBackgroundWorkRemovedAction, ChatChangesetsChangedAction, ChatWorkingDirectorySetAction, ChatWorkingDirectoryRemovedAction, @@ -166,6 +168,8 @@ export const enum ActionType { ChatError = 'chat/error', ChatTurnResume = 'chat/turnResume', ChatActivityChanged = 'chat/activityChanged', + ChatBackgroundWorkSet = 'chat/backgroundWorkSet', + ChatBackgroundWorkRemoved = 'chat/backgroundWorkRemoved', ChatChangesetsChanged = 'chat/changesetsChanged', ChatWorkingDirectorySet = 'chat/workingDirectorySet', ChatWorkingDirectoryRemoved = 'chat/workingDirectoryRemoved', @@ -326,6 +330,8 @@ export type StateAction = | ChatErrorAction | ChatTurnResumeAction | ChatActivityChangedAction + | ChatBackgroundWorkSetAction + | ChatBackgroundWorkRemovedAction | ChatChangesetsChangedAction | ChatWorkingDirectorySetAction | ChatWorkingDirectoryRemovedAction diff --git a/types/test-cases/reducers/277-chat-backgroundworkset-adds.json b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json new file mode 100644 index 000000000..5985e805f --- /dev/null +++ b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json @@ -0,0 +1,60 @@ +{ + "description": "setting background work appends shells and subagents in order", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [] + }, + "actions": [ + { + "type": "chat/backgroundWorkSet", + "work": { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + } + }, + { + "type": "chat/backgroundWorkSet", + "work": { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" + } + } + ], + "expected": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + }, + { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" + } + ] + } +} diff --git a/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json b/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json new file mode 100644 index 000000000..18bc61672 --- /dev/null +++ b/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json @@ -0,0 +1,51 @@ +{ + "description": "setting background work replaces by ID without duplication", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + } + ] + }, + "actions": [ + { + "type": "chat/backgroundWorkSet", + "work": { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "idle", + "startedAt": "2026-09-25T00:00:00.000Z" + } + } + ], + "expected": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "idle", + "startedAt": "2026-09-25T00:00:00.000Z" + } + ] + } +} diff --git a/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json new file mode 100644 index 000000000..48fdbff43 --- /dev/null +++ b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json @@ -0,0 +1,56 @@ +{ + "description": "removing finished background work keeps other entries and chat status", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + }, + { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" + } + ] + }, + "actions": [ + { + "type": "chat/backgroundWorkRemoved", + "id": "shell-1" + }, + { + "type": "chat/backgroundWorkRemoved", + "id": "missing" + } + ], + "expected": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" + } + ] + } +} diff --git a/types/test-cases/reducers/280-chat-backgroundworkremoved-absent.json b/types/test-cases/reducers/280-chat-backgroundworkremoved-absent.json new file mode 100644 index 000000000..95d84cec2 --- /dev/null +++ b/types/test-cases/reducers/280-chat-backgroundworkremoved-absent.json @@ -0,0 +1,24 @@ +{ + "description": "removing background work from a chat without a list is a no-op", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [] + }, + "actions": [ + { + "type": "chat/backgroundWorkRemoved", + "id": "missing" + } + ], + "expected": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [] + } +} diff --git a/types/test-cases/reducers/281-session-chatupdated-backgroundwork.json b/types/test-cases/reducers/281-session-chatupdated-backgroundwork.json new file mode 100644 index 000000000..eccd9e742 --- /dev/null +++ b/types/test-cases/reducers/281-session-chatupdated-backgroundwork.json @@ -0,0 +1,62 @@ +{ + "description": "session subscribers receive the chat's background work", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [ + { + "resource": "ahp-chat:/session/main", + "title": "Main", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z" + } + ] + }, + "actions": [ + { + "type": "session/chatUpdated", + "chat": "ahp-chat:/session/main", + "changes": { + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + } + ] + } + } + ], + "expected": { + "provider": "copilot", + "title": "Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [ + { + "resource": "ahp-chat:/session/main", + "title": "Main", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + } + ] + } + ] + } +} diff --git a/types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json b/types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json new file mode 100644 index 000000000..a8c985d8d --- /dev/null +++ b/types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json @@ -0,0 +1,54 @@ +{ + "description": "an explicitly empty background work list clears a chat's previous entries", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [ + { + "resource": "ahp-chat:/session/main", + "title": "Main", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + } + ] + } + ] + }, + "actions": [ + { + "type": "session/chatUpdated", + "chat": "ahp-chat:/session/main", + "changes": { + "backgroundWork": [] + } + } + ], + "expected": { + "provider": "copilot", + "title": "Session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [ + { + "resource": "ahp-chat:/session/main", + "title": "Main", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "backgroundWork": [] + } + ] + } +} diff --git a/types/version/registry.ts b/types/version/registry.ts index 6cb75a7dc..bccbf61db 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -128,6 +128,8 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChatError]: '0.4.0', [ActionType.ChatTurnResume]: '0.9.0', [ActionType.ChatActivityChanged]: '0.5.0', + [ActionType.ChatBackgroundWorkSet]: '0.9.0', + [ActionType.ChatBackgroundWorkRemoved]: '0.9.0', [ActionType.ChatChangesetsChanged]: '0.9.0', [ActionType.ChatWorkingDirectorySet]: '0.7.0', [ActionType.ChatWorkingDirectoryRemoved]: '0.7.0', From c1c92d11f25279552d5276b6c0b6364f8f96aaae Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 10:20:14 -0700 Subject: [PATCH 02/14] Start background work with shells only Drop the subagent kind for now. The kind list is non-exhaustive, so subagents and other kinds can be added later without breaking clients. --- .../JsonSerializerContext.generated.cs | 1 - .../Generated/State.generated.cs | 38 +----------- .../dotnet/src/AgentHostProtocol/Reducers.cs | 1 - clients/go/ahp/reducers.go | 2 - clients/go/ahptypes/state.generated.go | 36 +---------- .../microsoft/agenthostprotocol/Reducers.kt | 1 - .../generated/State.generated.kt | 46 +------------- clients/rust/crates/ahp-types/src/state.rs | 33 +--------- clients/rust/crates/ahp/src/reducers.rs | 1 - .../Generated/State.generated.swift | 60 +------------------ .../Sources/AgentHostProtocol/Reducers.swift | 1 - .../20260929-chat-background-work.json | 2 +- docs/guide/state-model.md | 5 +- schema/actions.schema.json | 57 ++---------------- schema/commands.schema.json | 57 ++---------------- schema/errors.schema.json | 57 ++---------------- schema/notifications.schema.json | 57 ++---------------- schema/state.schema.json | 57 ++---------------- scripts/generate-csharp.ts | 2 - scripts/generate-go.ts | 2 - scripts/generate-kotlin.ts | 3 +- scripts/generate-rust.ts | 2 - scripts/generate-swift.ts | 3 +- types/channels-chat/state.ts | 21 +------ .../277-chat-backgroundworkset-adds.json | 21 +------ ...79-chat-backgroundworkremoved-removes.json | 21 +------ 26 files changed, 42 insertions(+), 545 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 916cf4095..d6ecc1467 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -77,7 +77,6 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(AutomationTriggerKind))] [JsonSerializable(typeof(AutomationUpdateRequestedAction))] [JsonSerializable(typeof(BackgroundShellWork))] -[JsonSerializable(typeof(BackgroundSubagentWork))] [JsonSerializable(typeof(BackgroundWork))] [JsonSerializable(typeof(BackgroundWorkKind))] [JsonSerializable(typeof(BackgroundWorkStatus))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index f10203e2e..1074b9c4f 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -467,9 +467,6 @@ public enum BackgroundWorkKind /// A shell command that continues after its initiating tool call returns. [WireValue("shell")] Shell, - /// A subagent running in the background. - [WireValue("subagent")] - Subagent, } /// Activity of background work that has not finished. @@ -1200,7 +1197,7 @@ public sealed record BackgroundShellWork /// convention. public required string Id { get; init; } - /// Human-readable label, such as the command's purpose or the subagent's name. + /// Human-readable label, such as the command's purpose. public required string Label { get; init; } /// Current activity of the unfinished work. @@ -1224,36 +1221,6 @@ public sealed record BackgroundShellWork public string? Terminal { get; init; } } -/// A subagent running in the background. Its own state lives in its chat. -public sealed record BackgroundSubagentWork -{ - /// Identifier of this entry, unique within the owning chat across all kinds. - /// The host derives it however it likes (for example from the kind plus the - /// agent's own task id); consumers MUST treat it as opaque. It is the key for - /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert - /// convention. - public required string Id { get; init; } - - /// Human-readable label, such as the command's purpose or the subagent's name. - public required string Label { get; init; } - - /// Current activity of the unfinished work. - public BackgroundWorkStatus Status { get; init; } - - /// ISO 8601 timestamp when the work started. - public required string StartedAt { get; init; } - - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. - [JsonPropertyName("_meta")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public Dictionary? Meta { get; init; } - - public BackgroundWorkKind Kind { get; init; } - - /// The subagent's chat. - public required string Chat { get; init; } -} - /// Full state for a single chat, loaded when a client subscribes to the chat's /// URI. /// @@ -1281,7 +1248,7 @@ public sealed class ChatState public string? Activity { get; set; } /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells and subagents. Independent of turn state. + /// finishes, such as background shells. Independent of turn state. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? BackgroundWork { get; set; } @@ -6275,7 +6242,6 @@ public BackgroundWorkConverter() variants: new Dictionary { ["shell"] = typeof(BackgroundShellWork), - ["subagent"] = typeof(BackgroundSubagentWork), }, allowUnknown: true) { diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs index 218578a53..45e75827e 100644 --- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs +++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs @@ -94,7 +94,6 @@ private static SessionStatus WithInputNeededStatus(SessionStatus status, List work.Value switch { BackgroundShellWork v => v.Id, - BackgroundSubagentWork v => v.Id, JsonElement e when e.TryGetProperty("id", out JsonElement id) => id.GetString() ?? string.Empty, _ => string.Empty, }; diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index df262f255..092d7ba3f 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -327,8 +327,6 @@ func backgroundWorkID(w ahptypes.BackgroundWork) (string, bool) { switch v := w.Value.(type) { case *ahptypes.BackgroundShellWork: return v.Id, true - case *ahptypes.BackgroundSubagentWork: - return v.Id, true } return "", false } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index df6e88253..b8b7e118f 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -368,8 +368,6 @@ type BackgroundWorkKind string const ( // A shell command that continues after its initiating tool call returns. BackgroundWorkKindShell BackgroundWorkKind = "shell" - // A subagent running in the background. - BackgroundWorkKindSubagent BackgroundWorkKind = "subagent" ) // Activity of background work that has not finished. @@ -1271,7 +1269,7 @@ type ChatState struct { // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` // Work running outside the current turn that will resume this chat when it - // finishes, such as background shells and subagents. Independent of turn state. + // finishes, such as background shells. Independent of turn state. BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` @@ -1372,7 +1370,7 @@ type BackgroundShellWork struct { // the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert // convention. Id string `json:"id"` - // Human-readable label, such as the command's purpose or the subagent's name. + // Human-readable label, such as the command's purpose. Label string `json:"label"` // Current activity of the unfinished work. Status BackgroundWorkStatus `json:"status"` @@ -1387,27 +1385,6 @@ type BackgroundShellWork struct { Terminal *URI `json:"terminal,omitempty"` } -// A subagent running in the background. Its own state lives in its chat. -type BackgroundSubagentWork struct { - // Identifier of this entry, unique within the owning chat across all kinds. - // The host derives it however it likes (for example from the kind plus the - // agent's own task id); consumers MUST treat it as opaque. It is the key for - // the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert - // convention. - Id string `json:"id"` - // Human-readable label, such as the command's purpose or the subagent's name. - Label string `json:"label"` - // Current activity of the unfinished work. - Status BackgroundWorkStatus `json:"status"` - // ISO 8601 timestamp when the work started. - StartedAt string `json:"startedAt"` - // Provider-specific metadata, such as how a shell's lifetime is tied to its agent. - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - Kind BackgroundWorkKind `json:"kind"` - // The subagent's chat. - Chat URI `json:"chat"` -} - // Immutable selected-text snapshot captured when a side chat is created. // // The host records this exact text when it accepts `createChat`; later changes @@ -5693,8 +5670,7 @@ type BackgroundWork struct { // concrete variant of BackgroundWork. type isBackgroundWork interface{ isBackgroundWork() } -func (*BackgroundShellWork) isBackgroundWork() {} -func (*BackgroundSubagentWork) isBackgroundWork() {} +func (*BackgroundShellWork) isBackgroundWork() {} // BackgroundWorkUnknown carries an unrecognized BackgroundWork 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 BackgroundWorkUnknown struct { @@ -5716,12 +5692,6 @@ func (u *BackgroundWork) UnmarshalJSON(data []byte) error { return err } u.Value = &value - case "subagent": - var value BackgroundSubagentWork - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value default: raw := make(json.RawMessage, len(data)) copy(raw, data) 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 ff4ec35bb..569530a54 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -255,7 +255,6 @@ private fun customizationId(c: Customization): String? = when (c) { private fun backgroundWorkId(w: BackgroundWork): String? = when (w) { is BackgroundWorkShell -> w.value.id - is BackgroundWorkSubagent -> w.value.id // Unknown variants carry an opaque `raw` JSON object — no id to expose. is BackgroundWorkUnknown -> 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 6a7ed3350..70ff010fb 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 @@ -780,10 +780,6 @@ value class BackgroundWorkKind(val rawValue: String) { * A shell command that continues after its initiating tool call returns. */ val SHELL: BackgroundWorkKind = BackgroundWorkKind("shell") - /** - * A subagent running in the background. - */ - val SUBAGENT: BackgroundWorkKind = BackgroundWorkKind("subagent") } } @@ -1666,7 +1662,7 @@ data class ChatState( val activity: String? = null, /** * Work running outside the current turn that will resume this chat when it - * finishes, such as background shells and subagents. Independent of turn state. + * finishes, such as background shells. Independent of turn state. */ val backgroundWork: List? = null, /** @@ -2001,7 +1997,7 @@ data class BackgroundShellWork( */ val id: String, /** - * Human-readable label, such as the command's purpose or the subagent's name. + * Human-readable label, such as the command's purpose. */ val label: String, /** @@ -2028,40 +2024,6 @@ data class BackgroundShellWork( val terminal: String? = null ) -@Serializable -data class BackgroundSubagentWork( - /** - * Identifier of this entry, unique within the owning chat across all kinds. - * The host derives it however it likes (for example from the kind plus the - * agent's own task id); consumers MUST treat it as opaque. It is the key for - * the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert - * convention. - */ - val id: String, - /** - * Human-readable label, such as the command's purpose or the subagent's name. - */ - val label: String, - /** - * Current activity of the unfinished work. - */ - val status: BackgroundWorkStatus, - /** - * ISO 8601 timestamp when the work started. - */ - val startedAt: String, - /** - * Provider-specific metadata, such as how a shell's lifetime is tied to its agent. - */ - @SerialName("_meta") - val meta: Map? = null, - val kind: BackgroundWorkKind, - /** - * The subagent's chat. - */ - val chat: String -) - @Serializable data class SessionChatInputRequest( /** @@ -7003,8 +6965,6 @@ sealed interface BackgroundWork @JvmInline value class BackgroundWorkShell(val value: BackgroundShellWork) : BackgroundWork -@JvmInline -value class BackgroundWorkSubagent(val value: BackgroundSubagentWork) : BackgroundWork /** * Forward-compat catch-all for unknown BackgroundWork discriminators. * @@ -7030,7 +6990,6 @@ internal object BackgroundWorkSerializer : KSerializer { ?: return BackgroundWorkUnknown(obj) return when (discriminant) { "shell" -> BackgroundWorkShell(input.json.decodeFromJsonElement(BackgroundShellWork.serializer(), element)) - "subagent" -> BackgroundWorkSubagent(input.json.decodeFromJsonElement(BackgroundSubagentWork.serializer(), element)) else -> BackgroundWorkUnknown(obj) } } @@ -7040,7 +6999,6 @@ internal object BackgroundWorkSerializer : KSerializer { ?: error("BackgroundWork can only be serialized to JSON") val element: JsonElement = when (value) { is BackgroundWorkShell -> output.json.encodeToJsonElement(BackgroundShellWork.serializer(), value.value) - is BackgroundWorkSubagent -> output.json.encodeToJsonElement(BackgroundSubagentWork.serializer(), value.value) is BackgroundWorkUnknown -> value.raw } output.encodeJsonElement(element) diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 2ce4fc184..a2bdee892 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1002,8 +1002,6 @@ pub enum TerminalLifecycleStatus { pub enum BackgroundWorkKind { /// A shell command that continues after its initiating tool call returns. Shell, - /// A subagent running in the background. - Subagent, /// Unknown raw value from a newer protocol version, preserved verbatim. Unknown(String), } @@ -1015,7 +1013,6 @@ impl serde::Serialize for BackgroundWorkKind { { match self { Self::Shell => serializer.serialize_str("shell"), - Self::Subagent => serializer.serialize_str("subagent"), Self::Unknown(value) => serializer.serialize_str(value), } } @@ -1029,7 +1026,6 @@ impl<'de> serde::Deserialize<'de> for BackgroundWorkKind { let raw = ::deserialize(deserializer)?; Ok(match raw.as_str() { "shell" => Self::Shell, - "subagent" => Self::Subagent, _ => Self::Unknown(raw), }) } @@ -1965,7 +1961,7 @@ pub struct ChatState { #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells and subagents. Independent of turn state. + /// finishes, such as background shells. Independent of turn state. #[serde(default, skip_serializing_if = "Option::is_none")] pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) @@ -2086,7 +2082,7 @@ pub struct BackgroundShellWork { /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert /// convention. pub id: String, - /// Human-readable label, such as the command's purpose or the subagent's name. + /// Human-readable label, such as the command's purpose. pub label: String, /// Current activity of the unfinished work. pub status: BackgroundWorkStatus, @@ -2102,29 +2098,6 @@ pub struct BackgroundShellWork { pub terminal: Option, } -/// A subagent running in the background. Its own state lives in its chat. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BackgroundSubagentWork { - /// Identifier of this entry, unique within the owning chat across all kinds. - /// The host derives it however it likes (for example from the kind plus the - /// agent's own task id); consumers MUST treat it as opaque. It is the key for - /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert - /// convention. - pub id: String, - /// Human-readable label, such as the command's purpose or the subagent's name. - pub label: String, - /// Current activity of the unfinished work. - pub status: BackgroundWorkStatus, - /// ISO 8601 timestamp when the work started. - pub started_at: String, - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - /// The subagent's chat. - pub chat: Uri, -} - /// Immutable selected-text snapshot captured when a side chat is created. /// /// The host records this exact text when it accepts `createChat`; later changes @@ -6314,8 +6287,6 @@ pub enum SessionInputRequest { pub enum BackgroundWork { #[serde(rename = "shell")] Shell(BackgroundShellWork), - #[serde(rename = "subagent")] - Subagent(BackgroundSubagentWork), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 193abfcff..2961d7c15 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -486,7 +486,6 @@ fn session_input_request_id(r: &SessionInputRequest) -> Option<&str> { fn background_work_id(w: &BackgroundWork) -> Option<&str> { match w { BackgroundWork::Shell(x) => Some(x.id.as_str()), - BackgroundWork::Subagent(x) => Some(x.id.as_str()), BackgroundWork::Unknown(v) => v.get("id").and_then(serde_json::Value::as_str), } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 2dab5c7d4..f06a02e39 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -767,8 +767,6 @@ public enum TerminalLifecycleStatus: String, Codable, Sendable { public enum BackgroundWorkKind: Codable, Sendable, Equatable { /// A shell command that continues after its initiating tool call returns. case shell - /// A subagent running in the background. - case subagent /// Unknown raw value from a newer protocol version, preserved verbatim. case unknown(String) @@ -777,7 +775,6 @@ public enum BackgroundWorkKind: Codable, Sendable, Equatable { let raw = try container.decode(String.self) switch raw { case "shell": self = .shell - case "subagent": self = .subagent default: self = .unknown(raw) } } @@ -786,7 +783,6 @@ public enum BackgroundWorkKind: Codable, Sendable, Equatable { var container = encoder.singleValueContainer() switch self { case .shell: try container.encode("shell") - case .subagent: try container.encode("subagent") case .unknown(let raw): try container.encode(raw) } } @@ -1692,7 +1688,7 @@ public struct ChatState: Codable, Sendable { /// Human-readable description of what the chat is currently doing public var activity: String? /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells and subagents. Independent of turn state. + /// finishes, such as background shells. Independent of turn state. public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String @@ -2088,7 +2084,7 @@ public struct BackgroundShellWork: Codable, Sendable { /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert /// convention. public var id: String - /// Human-readable label, such as the command's purpose or the subagent's name. + /// Human-readable label, such as the command's purpose. public var label: String /// Current activity of the unfinished work. public var status: BackgroundWorkStatus @@ -2134,54 +2130,6 @@ public struct BackgroundShellWork: Codable, Sendable { } } -public struct BackgroundSubagentWork: Codable, Sendable { - /// Identifier of this entry, unique within the owning chat across all kinds. - /// The host derives it however it likes (for example from the kind plus the - /// agent's own task id); consumers MUST treat it as opaque. It is the key for - /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert - /// convention. - public var id: String - /// Human-readable label, such as the command's purpose or the subagent's name. - public var label: String - /// Current activity of the unfinished work. - public var status: BackgroundWorkStatus - /// ISO 8601 timestamp when the work started. - public var startedAt: String - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. - public var meta: [String: AnyCodable]? - public var kind: BackgroundWorkKind - /// The subagent's chat. - public var chat: String - - enum CodingKeys: String, CodingKey { - case id - case label - case status - case startedAt - case meta = "_meta" - case kind - case chat - } - - public init( - id: String, - label: String, - status: BackgroundWorkStatus, - startedAt: String, - meta: [String: AnyCodable]? = nil, - kind: BackgroundWorkKind, - chat: String - ) { - self.id = id - self.label = label - self.status = status - self.startedAt = startedAt - self.meta = meta - self.kind = kind - self.chat = chat - } -} - public struct SessionChatInputRequest: Codable, Sendable { /// Stable key for this entry, unique within the session's /// {@link SessionState.inputNeeded} list. The host derives it however it likes @@ -7869,7 +7817,6 @@ public enum SessionInputRequest: Codable, Sendable { } public enum BackgroundWork: Codable, Sendable { case shell(BackgroundShellWork) - case subagent(BackgroundSubagentWork) /// Unknown or future discriminant; the raw payload is preserved /// and re-encoded verbatim for forward-compatibility. case unknown(AnyCodable) @@ -7887,8 +7834,6 @@ public enum BackgroundWork: Codable, Sendable { switch discriminant { case "shell": self = .shell(try BackgroundShellWork(from: decoder)) - case "subagent": - self = .subagent(try BackgroundSubagentWork(from: decoder)) default: self = .unknown(try AnyCodable(from: decoder)) } @@ -7897,7 +7842,6 @@ public enum BackgroundWork: Codable, Sendable { public func encode(to encoder: Encoder) throws { switch self { case .shell(let value): try value.encode(to: encoder) - case .subagent(let value): try value.encode(to: encoder) case .unknown(let value): try value.encode(to: encoder) } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 0ae9349a3..930c6c53e 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -76,7 +76,6 @@ private func refineToolCallContributor(_ current: ToolCallContributor?, _ next: private func backgroundWorkID(_ w: BackgroundWork) -> String? { switch w { case .shell(let x): return x.id - case .subagent(let x): return x.id case .unknown: return nil } } diff --git a/docs/.changes/20260929-chat-background-work.json b/docs/.changes/20260929-chat-background-work.json index 3a534a88f..930870ff2 100644 --- a/docs/.changes/20260929-chat-background-work.json +++ b/docs/.changes/20260929-chat-background-work.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Expose chat-owned background work (background shells and subagents) in chat state and session chat summaries, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime." + "message": "Expose chat-owned background work, starting with background shells, in chat state and session chat summaries, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime." } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 217225481..4d85bdebd 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -142,7 +142,7 @@ For example, `(status & SessionStatus.InProgress) !== 0` is true for both `InPro Subscribable on a [Chat Channel](/specification/chat-channel) at `ahp-chat:/`. A session is a catalog of chats (`SessionState.chats`); each chat carries the per-conversation state — the turn history, the active turn and its streaming response parts (including live input requests), tool calls, steering/queued messages, and the user's in-progress draft. A session starts with a default chat (`SessionState.defaultChat`); hosts advertising the `multipleChats` capability let clients open more via `createChat`. `backgroundWork` lists work running outside the current turn that will resume the -chat when it finishes: background shells, background subagents, and future kinds. +chat when it finishes, starting with background shells. Hosts publish complete entries with `chat/backgroundWorkSet` and remove them with `chat/backgroundWorkRemoved` when they finish or are no longer tracked. Entry IDs are opaque, unique within a chat across all kinds, and scoped to that chat. Hosts mirror @@ -151,8 +151,7 @@ subscriber can discover its chats' background work without reading transcripts. Each entry has a `kind`, a `label`, a `status` (running or idle, for example a shell waiting for input), and a start time. A shell entry adds its plain-text command and, -when the host provides one, a terminal channel for its output. A subagent entry points -to the subagent's own chat instead of repeating its state. The kind set is +when the host provides one, a terminal channel for its output. The kind set is non-exhaustive: clients should keep entries of unknown kinds and may render them from the common fields. Provider-specific details, such as how a shell's lifetime is tied to its agent, belong in `_meta`. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 0bfa516e2..12c18c365 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -4970,7 +4970,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -5106,7 +5106,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -5139,7 +5139,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -5175,48 +5175,6 @@ "command" ] }, - "BackgroundSubagentWork": { - "type": "object", - "description": "A subagent running in the background. Its own state lives in its chat.", - "properties": { - "id": { - "type": "string", - "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." - }, - "label": { - "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." - }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, - "startedAt": { - "type": "string", - "description": "ISO 8601 timestamp when the work started." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." - }, - "kind": { - "const": "subagent" - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The subagent's chat." - } - }, - "required": [ - "id", - "label", - "status", - "startedAt", - "kind", - "chat" - ] - }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -8557,14 +8515,7 @@ "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, "BackgroundWork": { - "oneOf": [ - { - "$ref": "#/$defs/BackgroundShellWork" - }, - { - "$ref": "#/$defs/BackgroundSubagentWork" - } - ], + "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "ChatOrigin": { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 61be4cedb..9c207ff5c 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4080,7 +4080,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -4216,7 +4216,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -4249,7 +4249,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -4285,48 +4285,6 @@ "command" ] }, - "BackgroundSubagentWork": { - "type": "object", - "description": "A subagent running in the background. Its own state lives in its chat.", - "properties": { - "id": { - "type": "string", - "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." - }, - "label": { - "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." - }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, - "startedAt": { - "type": "string", - "description": "ISO 8601 timestamp when the work started." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." - }, - "kind": { - "const": "subagent" - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The subagent's chat." - } - }, - "required": [ - "id", - "label", - "status", - "startedAt", - "kind", - "chat" - ] - }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -10675,14 +10633,7 @@ "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, "BackgroundWork": { - "oneOf": [ - { - "$ref": "#/$defs/BackgroundShellWork" - }, - { - "$ref": "#/$defs/BackgroundSubagentWork" - } - ], + "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "BackgroundWorkStatus": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index a68783ff9..28b436307 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2509,7 +2509,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -2645,7 +2645,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -2678,7 +2678,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -2714,48 +2714,6 @@ "command" ] }, - "BackgroundSubagentWork": { - "type": "object", - "description": "A subagent running in the background. Its own state lives in its chat.", - "properties": { - "id": { - "type": "string", - "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." - }, - "label": { - "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." - }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, - "startedAt": { - "type": "string", - "description": "ISO 8601 timestamp when the work started." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." - }, - "kind": { - "const": "subagent" - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The subagent's chat." - } - }, - "required": [ - "id", - "label", - "status", - "startedAt", - "kind", - "chat" - ] - }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -7876,14 +7834,7 @@ "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, "BackgroundWork": { - "oneOf": [ - { - "$ref": "#/$defs/BackgroundShellWork" - }, - { - "$ref": "#/$defs/BackgroundSubagentWork" - } - ], + "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "BackgroundWorkStatus": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 821628a7c..f9df9e2e1 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2687,7 +2687,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -2823,7 +2823,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -2856,7 +2856,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -2892,48 +2892,6 @@ "command" ] }, - "BackgroundSubagentWork": { - "type": "object", - "description": "A subagent running in the background. Its own state lives in its chat.", - "properties": { - "id": { - "type": "string", - "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." - }, - "label": { - "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." - }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, - "startedAt": { - "type": "string", - "description": "ISO 8601 timestamp when the work started." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." - }, - "kind": { - "const": "subagent" - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The subagent's chat." - } - }, - "required": [ - "id", - "label", - "status", - "startedAt", - "kind", - "chat" - ] - }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -6402,14 +6360,7 @@ "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, "BackgroundWork": { - "oneOf": [ - { - "$ref": "#/$defs/BackgroundShellWork" - }, - { - "$ref": "#/$defs/BackgroundSubagentWork" - } - ], + "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "BackgroundWorkStatus": { diff --git a/schema/state.schema.json b/schema/state.schema.json index 6f4b2819a..eb905f904 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2420,7 +2420,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -2556,7 +2556,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -2589,7 +2589,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." + "description": "Human-readable label, such as the command's purpose." }, "status": { "$ref": "#/$defs/BackgroundWorkStatus", @@ -2625,48 +2625,6 @@ "command" ] }, - "BackgroundSubagentWork": { - "type": "object", - "description": "A subagent running in the background. Its own state lives in its chat.", - "properties": { - "id": { - "type": "string", - "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." - }, - "label": { - "type": "string", - "description": "Human-readable label, such as the command's purpose or the subagent's name." - }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, - "startedAt": { - "type": "string", - "description": "ISO 8601 timestamp when the work started." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." - }, - "kind": { - "const": "subagent" - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The subagent's chat." - } - }, - "required": [ - "id", - "label", - "status", - "startedAt", - "kind", - "chat" - ] - }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -6007,14 +5965,7 @@ "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, "BackgroundWork": { - "oneOf": [ - { - "$ref": "#/$defs/BackgroundShellWork" - }, - { - "$ref": "#/$defs/BackgroundSubagentWork" - } - ], + "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "ChatOrigin": { diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index 9a17b8b43..e0a03dddc 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -669,7 +669,6 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin { name: 'PendingMessage' }, { name: 'ChatSummary', mutable: true }, { name: 'BackgroundShellWork' }, - { name: 'BackgroundSubagentWork' }, { name: 'ChatState', mutable: true }, { name: 'ChatInputOption' }, { name: 'ChatInputTextQuestion' }, @@ -1166,7 +1165,6 @@ const BACKGROUND_WORK_UNION: UnionConfig = { doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, - { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 861d3bffd..18a0dfcf3 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -761,7 +761,6 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'ChatState' }, { name: 'ChatSummary' }, { name: 'BackgroundShellWork' }, - { name: 'BackgroundSubagentWork' }, { name: 'SideChatSelection' }, { name: 'PendingMessage' }, { name: 'ProjectInfo' }, @@ -1138,7 +1137,6 @@ const BACKGROUND_WORK_UNION: UnionConfig = { doc: 'BackgroundWork is work running outside the current turn that will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, - { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 6c09aa5d9..c55f84ae6 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -991,7 +991,7 @@ const STATE_STRUCTS = [ 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', - 'BackgroundShellWork', 'BackgroundSubagentWork', + 'BackgroundShellWork', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionToolAuthenticationRequest', 'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', @@ -1331,7 +1331,6 @@ const BACKGROUND_WORK_UNION: UnionConfig = { discriminantField: 'kind', variants: [ { caseName: 'Shell', structName: 'BackgroundShellWork', discriminantValue: 'shell' }, - { caseName: 'Subagent', structName: 'BackgroundSubagentWork', discriminantValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 5190771d1..7662cea58 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -811,7 +811,6 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'ChatState' }, { name: 'ChatSummary' }, { name: 'BackgroundShellWork', omitDiscriminants: true }, - { name: 'BackgroundSubagentWork', omitDiscriminants: true }, { name: 'SideChatSelection' }, { name: 'SessionState' }, { name: 'SessionActiveClient' }, @@ -1201,7 +1200,6 @@ const BACKGROUND_WORK_UNION: UnionConfig = { doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, - { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 2308eb76f..93dd5929c 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -696,7 +696,7 @@ const STATE_STRUCTS = [ 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', - 'BackgroundShellWork', 'BackgroundSubagentWork', + 'BackgroundShellWork', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionToolAuthenticationRequest', 'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', @@ -988,7 +988,6 @@ const BACKGROUND_WORK_UNION: UnionConfig = { allowUnknown: true, variants: [ { caseName: 'shell', structName: 'BackgroundShellWork', discriminantValue: 'shell' }, - { caseName: 'subagent', structName: 'BackgroundSubagentWork', discriminantValue: 'subagent' }, ], }; diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 527a4e6b2..d82195f4c 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -51,7 +51,7 @@ export interface ChatState { activity?: string; /** * Work running outside the current turn that will resume this chat when it - * finishes, such as background shells and subagents. Independent of turn state. + * finishes, such as background shells. Independent of turn state. */ backgroundWork?: BackgroundWork[]; /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -179,8 +179,6 @@ export interface ChatSummary { export const enum BackgroundWorkKind { /** A shell command that continues after its initiating tool call returns. */ Shell = 'shell', - /** A subagent running in the background. */ - Subagent = 'subagent', } /** @@ -209,7 +207,7 @@ interface BackgroundWorkBase { * convention. */ id: string; - /** Human-readable label, such as the command's purpose or the subagent's name. */ + /** Human-readable label, such as the command's purpose. */ label: string; /** Current activity of the unfinished work. */ status: BackgroundWorkStatus; @@ -232,17 +230,6 @@ export interface BackgroundShellWork extends BackgroundWorkBase { terminal?: URI; } -/** - * A subagent running in the background. Its own state lives in its chat. - * - * @category Background Work - */ -export interface BackgroundSubagentWork extends BackgroundWorkBase { - kind: BackgroundWorkKind.Subagent; - /** The subagent's chat. */ - chat: URI; -} - /** * Work running outside the current turn that will resume the owning chat when * it finishes. Clients that don't recognize a `kind` should keep the entry and @@ -250,9 +237,7 @@ export interface BackgroundSubagentWork extends BackgroundWorkBase { * * @category Background Work */ -export type BackgroundWork = - | BackgroundShellWork - | BackgroundSubagentWork; +export type BackgroundWork = BackgroundShellWork; /** * Discriminant for {@link ChatOrigin} — how a chat came into existence. diff --git a/types/test-cases/reducers/277-chat-backgroundworkset-adds.json b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json index 5985e805f..13a9e1729 100644 --- a/types/test-cases/reducers/277-chat-backgroundworkset-adds.json +++ b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json @@ -1,5 +1,5 @@ { - "description": "setting background work appends shells and subagents in order", + "description": "setting background work appends a new entry", "reducer": "chat", "initial": { "resource": "ahp-chat:/session/main", @@ -19,17 +19,6 @@ "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } - }, - { - "type": "chat/backgroundWorkSet", - "work": { - "kind": "subagent", - "id": "subagent-1", - "label": "Review changes", - "status": "running", - "startedAt": "2026-09-25T00:01:00.000Z", - "chat": "ahp-chat:/session/subagent-tool-1" - } } ], "expected": { @@ -46,14 +35,6 @@ "command": "npm test", "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" - }, - { - "kind": "subagent", - "id": "subagent-1", - "label": "Review changes", - "status": "running", - "startedAt": "2026-09-25T00:01:00.000Z", - "chat": "ahp-chat:/session/subagent-tool-1" } ] } diff --git a/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json index 48fdbff43..b864b046c 100644 --- a/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json +++ b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json @@ -1,5 +1,5 @@ { - "description": "removing finished background work keeps other entries and chat status", + "description": "finished background work is removed without changing chat status", "reducer": "chat", "initial": { "resource": "ahp-chat:/session/main", @@ -15,14 +15,6 @@ "command": "npm test", "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" - }, - { - "kind": "subagent", - "id": "subagent-1", - "label": "Review changes", - "status": "running", - "startedAt": "2026-09-25T00:01:00.000Z", - "chat": "ahp-chat:/session/subagent-tool-1" } ] }, @@ -42,15 +34,6 @@ "status": 1, "modifiedAt": "2026-09-25T00:00:00.000Z", "turns": [], - "backgroundWork": [ - { - "kind": "subagent", - "id": "subagent-1", - "label": "Review changes", - "status": "running", - "startedAt": "2026-09-25T00:01:00.000Z", - "chat": "ahp-chat:/session/subagent-tool-1" - } - ] + "backgroundWork": [] } } From a79717a3277512bf0f43b1d906a96a834cf6bce8 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 10:30:12 -0700 Subject: [PATCH 03/14] Format Rust reducer imports --- clients/rust/crates/ahp/src/reducers.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 2961d7c15..5102f5201 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -57,12 +57,12 @@ use ahp_types::actions::{ ChatTurnStartedAction, StateAction, }; use ahp_types::state::{ - ActiveTurn, AnnotationsState, AutomationRunState, AutomationState, BackgroundWork, ChangesetOperationStatus, - ChangesetState, ChangesetStatus, ChatInputRequest, ChatState, ChildCustomization, - ConfirmationOption, Customization, CustomizationEnablement, ErrorResponsePart, - InputRequestResponsePart, McpServerCustomization, McpServerStartingState, McpServerState, - McpServerStoppedState, PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, - RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, + ActiveTurn, AnnotationsState, AutomationRunState, AutomationState, BackgroundWork, + ChangesetOperationStatus, ChangesetState, ChangesetStatus, ChatInputRequest, ChatState, + ChildCustomization, ConfirmationOption, Customization, CustomizationEnablement, + ErrorResponsePart, InputRequestResponsePart, McpServerCustomization, McpServerStartingState, + McpServerState, McpServerStoppedState, PendingMessage, PendingMessageKind, ResourceWatchState, + ResponsePart, RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, TerminalContentPart, TerminalExitedLifecycleState, TerminalLifecycleState, TerminalState, TerminalUnclassifiedPart, ToolCallAuthRequiredState, ToolCallCancellationReason, ToolCallCancelledState, ToolCallCompletedState, ToolCallConfirmationReason, From 3846ab02da98197386fd07d9b825f91dcd7b7600 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 10:58:09 -0700 Subject: [PATCH 04/14] Keep background work of unknown kinds in every client The Go, Kotlin, and Swift reducers now read the common id from kinds they don't recognize, as Rust and .NET already do. A newer host's entries are kept on set and can be removed, matching the documented behavior. Add shared fixtures for both. --- clients/go/ahp/reducers.go | 8 +++ .../microsoft/agenthostprotocol/Reducers.kt | 5 +- .../Sources/AgentHostProtocol/Reducers.swift | 3 +- ...-backgroundworkset-keeps-unknown-kind.json | 70 +++++++++++++++++++ ...roundworkremoved-removes-unknown-kind.json | 52 ++++++++++++++ 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json create mode 100644 types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 092d7ba3f..d560d5085 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -327,6 +327,14 @@ func backgroundWorkID(w ahptypes.BackgroundWork) (string, bool) { switch v := w.Value.(type) { case *ahptypes.BackgroundShellWork: return v.Id, true + case *ahptypes.BackgroundWorkUnknown: + // Kinds from newer hosts still carry the common `id`, so they can be replaced and removed. + var common struct { + ID string `json:"id"` + } + if err := json.Unmarshal(v.Raw, &common); err == nil && common.ID != "" { + return common.ID, true + } } return "", false } 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 569530a54..73fe02ccd 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -11,6 +11,7 @@ import com.microsoft.agenthostprotocol.generated.* import java.time.Instant import java.time.format.DateTimeFormatterBuilder import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive // ─── Reducer Interface ────────────────────────────────────────────────────── @@ -255,8 +256,8 @@ private fun customizationId(c: Customization): String? = when (c) { private fun backgroundWorkId(w: BackgroundWork): String? = when (w) { is BackgroundWorkShell -> w.value.id - // Unknown variants carry an opaque `raw` JSON object — no id to expose. - is BackgroundWorkUnknown -> null + // Kinds from newer hosts still carry the common `id`, so they can be replaced and removed. + is BackgroundWorkUnknown -> (w.raw["id"] as? JsonPrimitive)?.takeIf { it.isString }?.content } private fun sessionInputRequestId(r: SessionInputRequest): String? = when (r) { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 930c6c53e..47f5e516c 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -76,7 +76,8 @@ private func refineToolCallContributor(_ current: ToolCallContributor?, _ next: private func backgroundWorkID(_ w: BackgroundWork) -> String? { switch w { case .shell(let x): return x.id - case .unknown: return nil + // Kinds from newer hosts still carry the common `id`, so they can be replaced and removed. + case .unknown(let raw): return (raw.value as? [String: Any])?["id"] as? String } } diff --git a/types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json b/types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json new file mode 100644 index 000000000..d2624b2a0 --- /dev/null +++ b/types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json @@ -0,0 +1,70 @@ +{ + "description": "background work of an unknown kind is kept and replaced by id", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + } + ] + }, + "actions": [ + { + "type": "chat/backgroundWorkSet", + "work": { + "kind": "futureKind", + "id": "future-1", + "label": "Start review", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "target": "reviewer" + } + }, + { + "type": "chat/backgroundWorkSet", + "work": { + "kind": "futureKind", + "id": "future-1", + "label": "Review changes", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "target": "reviewer" + } + } + ], + "expected": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + }, + { + "kind": "futureKind", + "id": "future-1", + "label": "Review changes", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "target": "reviewer" + } + ] + } +} diff --git a/types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json b/types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json new file mode 100644 index 000000000..95a40b4be --- /dev/null +++ b/types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json @@ -0,0 +1,52 @@ +{ + "description": "background work of an unknown kind is removed by id", + "reducer": "chat", + "initial": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + }, + { + "kind": "futureKind", + "id": "future-1", + "label": "Review changes", + "status": "running", + "startedAt": "2026-09-25T00:01:00.000Z", + "target": "reviewer" + } + ] + }, + "actions": [ + { + "type": "chat/backgroundWorkRemoved", + "id": "future-1" + } + ], + "expected": { + "resource": "ahp-chat:/session/main", + "title": "Test Chat", + "status": 1, + "modifiedAt": "2026-09-25T00:00:00.000Z", + "turns": [], + "backgroundWork": [ + { + "kind": "shell", + "id": "shell-1", + "label": "Run tests", + "command": "npm test", + "status": "running", + "startedAt": "2026-09-25T00:00:00.000Z" + } + ] + } +} From ecea39b42bff7d06f199567b3fc90c523ce6109c Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 11:02:54 -0700 Subject: [PATCH 05/14] Drop status from background work Every listed entry is unfinished work, and hosts remove finished work instead of marking it done. Copilot never reports a background shell as idle, so the field had no producer yet. An optional status can be added later without breaking clients. --- .../JsonSerializerContext.generated.cs | 1 - .../Generated/State.generated.cs | 14 ------- clients/go/ahptypes/state.generated.go | 11 ------ .../generated/State.generated.kt | 29 -------------- clients/rust/crates/ahp-types/src/state.rs | 39 ------------------- .../Generated/State.generated.swift | 33 ---------------- docs/guide/state-model.md | 12 +++--- schema/actions.schema.json | 18 --------- schema/commands.schema.json | 18 --------- schema/errors.schema.json | 18 --------- schema/notifications.schema.json | 18 --------- schema/state.schema.json | 18 --------- scripts/generate-csharp.ts | 2 +- scripts/generate-go.ts | 2 +- scripts/generate-kotlin.ts | 2 +- scripts/generate-rust.ts | 2 +- scripts/generate-swift.ts | 2 +- types/channels-chat/state.ts | 14 ------- .../277-chat-backgroundworkset-adds.json | 2 - .../278-chat-backgroundworkset-replaces.json | 7 +--- ...79-chat-backgroundworkremoved-removes.json | 1 - ...81-session-chatupdated-backgroundwork.json | 2 - ...ion-chatupdated-clears-backgroundwork.json | 1 - ...-backgroundworkset-keeps-unknown-kind.json | 5 --- ...roundworkremoved-removes-unknown-kind.json | 3 -- 25 files changed, 13 insertions(+), 261 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index d6ecc1467..b0a926f7b 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -79,7 +79,6 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(BackgroundShellWork))] [JsonSerializable(typeof(BackgroundWork))] [JsonSerializable(typeof(BackgroundWorkKind))] -[JsonSerializable(typeof(BackgroundWorkStatus))] [JsonSerializable(typeof(Changeset))] [JsonSerializable(typeof(ChangesetCapabilities))] [JsonSerializable(typeof(ChangesetClearedAction))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 1074b9c4f..87a0c5cb6 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -469,17 +469,6 @@ public enum BackgroundWorkKind Shell, } -/// Activity of background work that has not finished. -[JsonConverter(typeof(WireEnumConverter))] -public enum BackgroundWorkStatus -{ - [WireValue("running")] - Running, - /// Not making progress on its own, for example a shell waiting for input. - [WireValue("idle")] - Idle, -} - /// Discriminant for the {@link McpServerState} union. [JsonConverter(typeof(WireEnumConverter))] public enum McpServerStatus @@ -1200,9 +1189,6 @@ public sealed record BackgroundShellWork /// Human-readable label, such as the command's purpose. public required string Label { get; init; } - /// Current activity of the unfinished work. - public BackgroundWorkStatus Status { get; init; } - /// ISO 8601 timestamp when the work started. public required string StartedAt { get; init; } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index b8b7e118f..501aba8ee 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -370,15 +370,6 @@ const ( BackgroundWorkKindShell BackgroundWorkKind = "shell" ) -// Activity of background work that has not finished. -type BackgroundWorkStatus string - -const ( - BackgroundWorkStatusRunning BackgroundWorkStatus = "running" - // Not making progress on its own, for example a shell waiting for input. - BackgroundWorkStatusIdle BackgroundWorkStatus = "idle" -) - // Discriminant for the {@link McpServerState} union. type McpServerStatus string @@ -1372,8 +1363,6 @@ type BackgroundShellWork struct { Id string `json:"id"` // Human-readable label, such as the command's purpose. Label string `json:"label"` - // Current activity of the unfinished work. - Status BackgroundWorkStatus `json:"status"` // ISO 8601 timestamp when the work started. StartedAt string `json:"startedAt"` // Provider-specific metadata, such as how a shell's lifetime is tied to its agent. 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 70ff010fb..1ce2f346b 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 @@ -793,31 +793,6 @@ internal object BackgroundWorkKindSerializer : KSerializer { BackgroundWorkKind(decoder.decodeString()) } -/** - * Activity of background work that has not finished. - */ -@Serializable(with = BackgroundWorkStatusSerializer::class) -@JvmInline -value class BackgroundWorkStatus(val rawValue: String) { - companion object { - val RUNNING: BackgroundWorkStatus = BackgroundWorkStatus("running") - /** - * Not making progress on its own, for example a shell waiting for input. - */ - val IDLE: BackgroundWorkStatus = BackgroundWorkStatus("idle") - } -} - -internal object BackgroundWorkStatusSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("BackgroundWorkStatus", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: BackgroundWorkStatus) { - encoder.encodeString(value.rawValue) - } - override fun deserialize(decoder: Decoder): BackgroundWorkStatus = - BackgroundWorkStatus(decoder.decodeString()) -} - /** * Discriminant for the {@link McpServerState} union. */ @@ -2000,10 +1975,6 @@ data class BackgroundShellWork( * Human-readable label, such as the command's purpose. */ val label: String, - /** - * Current activity of the unfinished work. - */ - val status: BackgroundWorkStatus, /** * ISO 8601 timestamp when the work started. */ diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index a2bdee892..af39f5cb5 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1031,43 +1031,6 @@ impl<'de> serde::Deserialize<'de> for BackgroundWorkKind { } } -/// Activity of background work that has not finished. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum BackgroundWorkStatus { - Running, - /// Not making progress on its own, for example a shell waiting for input. - Idle, - /// Unknown raw value from a newer protocol version, preserved verbatim. - Unknown(String), -} - -impl serde::Serialize for BackgroundWorkStatus { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Self::Running => serializer.serialize_str("running"), - Self::Idle => serializer.serialize_str("idle"), - Self::Unknown(value) => serializer.serialize_str(value), - } - } -} - -impl<'de> serde::Deserialize<'de> for BackgroundWorkStatus { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let raw = ::deserialize(deserializer)?; - Ok(match raw.as_str() { - "running" => Self::Running, - "idle" => Self::Idle, - _ => Self::Unknown(raw), - }) - } -} - /// Discriminant for the {@link McpServerState} union. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum McpServerStatus { @@ -2084,8 +2047,6 @@ pub struct BackgroundShellWork { pub id: String, /// Human-readable label, such as the command's purpose. pub label: String, - /// Current activity of the unfinished work. - pub status: BackgroundWorkStatus, /// ISO 8601 timestamp when the work started. pub started_at: String, /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index f06a02e39..465ffc7d5 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -788,34 +788,6 @@ public enum BackgroundWorkKind: Codable, Sendable, Equatable { } } -/// Activity of background work that has not finished. -public enum BackgroundWorkStatus: Codable, Sendable, Equatable { - case running - /// Not making progress on its own, for example a shell waiting for input. - case idle - /// Unknown raw value from a newer protocol version, preserved verbatim. - case unknown(String) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let raw = try container.decode(String.self) - switch raw { - case "running": self = .running - case "idle": self = .idle - default: self = .unknown(raw) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .running: try container.encode("running") - case .idle: try container.encode("idle") - case .unknown(let raw): try container.encode(raw) - } - } -} - /// Discriminant for the {@link McpServerState} union. public enum McpServerStatus: Codable, Sendable, Equatable { /// Server has been registered but is not yet running. @@ -2086,8 +2058,6 @@ public struct BackgroundShellWork: Codable, Sendable { public var id: String /// Human-readable label, such as the command's purpose. public var label: String - /// Current activity of the unfinished work. - public var status: BackgroundWorkStatus /// ISO 8601 timestamp when the work started. public var startedAt: String /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. @@ -2101,7 +2071,6 @@ public struct BackgroundShellWork: Codable, Sendable { enum CodingKeys: String, CodingKey { case id case label - case status case startedAt case meta = "_meta" case kind @@ -2112,7 +2081,6 @@ public struct BackgroundShellWork: Codable, Sendable { public init( id: String, label: String, - status: BackgroundWorkStatus, startedAt: String, meta: [String: AnyCodable]? = nil, kind: BackgroundWorkKind, @@ -2121,7 +2089,6 @@ public struct BackgroundShellWork: Codable, Sendable { ) { self.id = id self.label = label - self.status = status self.startedAt = startedAt self.meta = meta self.kind = kind diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 4d85bdebd..36e5c3193 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -149,12 +149,12 @@ opaque, unique within a chat across all kinds, and scoped to that chat. Hosts mi the list into `SessionState.chats` through `session/chatUpdated`, so a session subscriber can discover its chats' background work without reading transcripts. -Each entry has a `kind`, a `label`, a `status` (running or idle, for example a shell -waiting for input), and a start time. A shell entry adds its plain-text command and, -when the host provides one, a terminal channel for its output. The kind set is -non-exhaustive: clients should keep entries of unknown kinds and may render them from -the common fields. Provider-specific details, such as how a shell's lifetime is tied -to its agent, belong in `_meta`. +Each entry has a `kind`, a `label`, and a start time. Every entry is unfinished work; +hosts remove entries when the work finishes rather than marking them done. A shell +entry adds its plain-text command and, when the host provides one, a terminal channel +for its output. The kind set is non-exhaustive: clients should keep entries of unknown +kinds and may render them from the common fields. Provider-specific details, such as +how a shell's lifetime is tied to its agent, belong in `_meta`. The collection survives turn completion, cancellation, steering, and history truncation; those actions do not establish whether the work has stopped. Hosts must diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 12c18c365..ce3bd3d99 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5108,10 +5108,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -5125,7 +5121,6 @@ "required": [ "id", "label", - "status", "startedAt" ] }, @@ -5141,10 +5136,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -5169,7 +5160,6 @@ "required": [ "id", "label", - "status", "startedAt", "kind", "command" @@ -9275,14 +9265,6 @@ "type": "string", "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, - "BackgroundWorkStatus": { - "enum": [ - "running", - "idle" - ], - "type": "string", - "description": "Activity of background work that has not finished." - }, "TurnState": { "enum": [ "complete", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 9c207ff5c..bd2bdbaae 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4218,10 +4218,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -4235,7 +4231,6 @@ "required": [ "id", "label", - "status", "startedAt" ] }, @@ -4251,10 +4246,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -4279,7 +4270,6 @@ "required": [ "id", "label", - "status", "startedAt", "kind", "command" @@ -10636,14 +10626,6 @@ "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, - "BackgroundWorkStatus": { - "enum": [ - "running", - "idle" - ], - "type": "string", - "description": "Activity of background work that has not finished." - }, "ChatInputQuestion": { "oneOf": [ { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 28b436307..f12d4610b 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2647,10 +2647,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -2664,7 +2660,6 @@ "required": [ "id", "label", - "status", "startedAt" ] }, @@ -2680,10 +2675,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -2708,7 +2699,6 @@ "required": [ "id", "label", - "status", "startedAt", "kind", "command" @@ -7837,14 +7827,6 @@ "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, - "BackgroundWorkStatus": { - "enum": [ - "running", - "idle" - ], - "type": "string", - "description": "Activity of background work that has not finished." - }, "ChatInputQuestion": { "oneOf": [ { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index f9df9e2e1..c91c12630 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2825,10 +2825,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -2842,7 +2838,6 @@ "required": [ "id", "label", - "status", "startedAt" ] }, @@ -2858,10 +2853,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -2886,7 +2877,6 @@ "required": [ "id", "label", - "status", "startedAt", "kind", "command" @@ -6363,14 +6353,6 @@ "$ref": "#/$defs/BackgroundShellWork", "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, - "BackgroundWorkStatus": { - "enum": [ - "running", - "idle" - ], - "type": "string", - "description": "Activity of background work that has not finished." - }, "ChatInputQuestion": { "oneOf": [ { diff --git a/schema/state.schema.json b/schema/state.schema.json index eb905f904..34b3dee92 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2558,10 +2558,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -2575,7 +2571,6 @@ "required": [ "id", "label", - "status", "startedAt" ] }, @@ -2591,10 +2586,6 @@ "type": "string", "description": "Human-readable label, such as the command's purpose." }, - "status": { - "$ref": "#/$defs/BackgroundWorkStatus", - "description": "Current activity of the unfinished work." - }, "startedAt": { "type": "string", "description": "ISO 8601 timestamp when the work started." @@ -2619,7 +2610,6 @@ "required": [ "id", "label", - "status", "startedAt", "kind", "command" @@ -6368,14 +6358,6 @@ "type": "string", "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, - "BackgroundWorkStatus": { - "enum": [ - "running", - "idle" - ], - "type": "string", - "description": "Activity of background work that has not finished." - }, "TurnState": { "enum": [ "complete", diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index e0a03dddc..d85525d50 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -642,7 +642,7 @@ const STATE_ENUMS = [ 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', - 'BackgroundWorkKind', 'BackgroundWorkStatus', + 'BackgroundWorkKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind', diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 18a0dfcf3..5e6638250 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -726,7 +726,7 @@ const STATE_ENUMS = [ 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', - 'BackgroundWorkKind', 'BackgroundWorkStatus', + 'BackgroundWorkKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index c55f84ae6..f26deadeb 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -975,7 +975,7 @@ const STATE_ENUMS = [ 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', - 'BackgroundWorkKind', 'BackgroundWorkStatus', + 'BackgroundWorkKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 7662cea58..b68634ccc 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -763,7 +763,7 @@ const STATE_ENUMS = [ 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', - 'BackgroundWorkKind', 'BackgroundWorkStatus', + 'BackgroundWorkKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 93dd5929c..fa2a0d273 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -680,7 +680,7 @@ const STATE_ENUMS = [ 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'TerminalLifecycleStatus', - 'BackgroundWorkKind', 'BackgroundWorkStatus', + 'BackgroundWorkKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index d82195f4c..6ad0bbc1a 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -181,18 +181,6 @@ export const enum BackgroundWorkKind { Shell = 'shell', } -/** - * Activity of background work that has not finished. - * - * @category Background Work - * @nonexhaustive - */ -export const enum BackgroundWorkStatus { - Running = 'running', - /** Not making progress on its own, for example a shell waiting for input. */ - Idle = 'idle', -} - /** * Fields common to every {@link BackgroundWork} variant. * @@ -209,8 +197,6 @@ interface BackgroundWorkBase { id: string; /** Human-readable label, such as the command's purpose. */ label: string; - /** Current activity of the unfinished work. */ - status: BackgroundWorkStatus; /** ISO 8601 timestamp when the work started. */ startedAt: string; /** Provider-specific metadata, such as how a shell's lifetime is tied to its agent. */ diff --git a/types/test-cases/reducers/277-chat-backgroundworkset-adds.json b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json index 13a9e1729..19b95d973 100644 --- a/types/test-cases/reducers/277-chat-backgroundworkset-adds.json +++ b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json @@ -16,7 +16,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } } @@ -33,7 +32,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] diff --git a/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json b/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json index 18bc61672..123519402 100644 --- a/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json +++ b/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json @@ -13,7 +13,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] @@ -24,9 +23,8 @@ "work": { "kind": "shell", "id": "shell-1", - "label": "Run tests", + "label": "Run unit tests", "command": "npm test", - "status": "idle", "startedAt": "2026-09-25T00:00:00.000Z" } } @@ -41,9 +39,8 @@ { "kind": "shell", "id": "shell-1", - "label": "Run tests", + "label": "Run unit tests", "command": "npm test", - "status": "idle", "startedAt": "2026-09-25T00:00:00.000Z" } ] diff --git a/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json index b864b046c..3193e0682 100644 --- a/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json +++ b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json @@ -13,7 +13,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] diff --git a/types/test-cases/reducers/281-session-chatupdated-backgroundwork.json b/types/test-cases/reducers/281-session-chatupdated-backgroundwork.json index eccd9e742..f9e5c0f44 100644 --- a/types/test-cases/reducers/281-session-chatupdated-backgroundwork.json +++ b/types/test-cases/reducers/281-session-chatupdated-backgroundwork.json @@ -27,7 +27,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] @@ -52,7 +51,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] diff --git a/types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json b/types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json index a8c985d8d..40623306e 100644 --- a/types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json +++ b/types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json @@ -19,7 +19,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] diff --git a/types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json b/types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json index d2624b2a0..e34ad14d3 100644 --- a/types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json +++ b/types/test-cases/reducers/283-chat-backgroundworkset-keeps-unknown-kind.json @@ -13,7 +13,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] @@ -25,7 +24,6 @@ "kind": "futureKind", "id": "future-1", "label": "Start review", - "status": "running", "startedAt": "2026-09-25T00:01:00.000Z", "target": "reviewer" } @@ -36,7 +34,6 @@ "kind": "futureKind", "id": "future-1", "label": "Review changes", - "status": "running", "startedAt": "2026-09-25T00:01:00.000Z", "target": "reviewer" } @@ -54,14 +51,12 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" }, { "kind": "futureKind", "id": "future-1", "label": "Review changes", - "status": "running", "startedAt": "2026-09-25T00:01:00.000Z", "target": "reviewer" } diff --git a/types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json b/types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json index 95a40b4be..323a5f683 100644 --- a/types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json +++ b/types/test-cases/reducers/284-chat-backgroundworkremoved-removes-unknown-kind.json @@ -13,14 +13,12 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" }, { "kind": "futureKind", "id": "future-1", "label": "Review changes", - "status": "running", "startedAt": "2026-09-25T00:01:00.000Z", "target": "reviewer" } @@ -44,7 +42,6 @@ "id": "shell-1", "label": "Run tests", "command": "npm test", - "status": "running", "startedAt": "2026-09-25T00:00:00.000Z" } ] From 1d4697bbd935069ef7c09150eccfe066e36bac02 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 13:06:07 -0700 Subject: [PATCH 06/14] Drop terminal from background shell work Nothing sets or reads it yet. What a background shell's terminal should point to belongs with output streaming, and an optional field can be added then without breaking clients. --- .../Generated/State.generated.cs | 4 ---- clients/go/ahptypes/state.generated.go | 2 -- .../agenthostprotocol/generated/State.generated.kt | 6 +----- clients/rust/crates/ahp-types/src/state.rs | 3 --- .../AgentHostProtocol/Generated/State.generated.swift | 7 +------ docs/guide/state-model.md | 7 +++---- schema/actions.schema.json | 4 ---- schema/commands.schema.json | 4 ---- schema/errors.schema.json | 4 ---- schema/notifications.schema.json | 4 ---- schema/state.schema.json | 4 ---- types/channels-chat/state.ts | 2 -- 12 files changed, 5 insertions(+), 46 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 87a0c5cb6..3ffcc560f 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1201,10 +1201,6 @@ public sealed record BackgroundShellWork /// Command line, displayed as plain text. public required string Command { get; init; } - - /// Terminal channel carrying this shell's output, when the host provides one. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Terminal { get; init; } } /// Full state for a single chat, loaded when a client subscribes to the chat's diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 501aba8ee..8a5383c72 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1370,8 +1370,6 @@ type BackgroundShellWork struct { Kind BackgroundWorkKind `json:"kind"` // Command line, displayed as plain text. Command string `json:"command"` - // Terminal channel carrying this shell's output, when the host provides one. - Terminal *URI `json:"terminal,omitempty"` } // Immutable selected-text snapshot captured when a side chat is created. 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 1ce2f346b..8b1f3cd81 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 @@ -1988,11 +1988,7 @@ data class BackgroundShellWork( /** * Command line, displayed as plain text. */ - val command: String, - /** - * Terminal channel carrying this shell's output, when the host provides one. - */ - val terminal: String? = null + val command: String ) @Serializable diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index af39f5cb5..2e56a7ec0 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2054,9 +2054,6 @@ pub struct BackgroundShellWork { pub meta: Option, /// Command line, displayed as plain text. pub command: String, - /// Terminal channel carrying this shell's output, when the host provides one. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub terminal: Option, } /// Immutable selected-text snapshot captured when a side chat is created. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 465ffc7d5..2a86a41ed 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -2065,8 +2065,6 @@ public struct BackgroundShellWork: Codable, Sendable { public var kind: BackgroundWorkKind /// Command line, displayed as plain text. public var command: String - /// Terminal channel carrying this shell's output, when the host provides one. - public var terminal: String? enum CodingKeys: String, CodingKey { case id @@ -2075,7 +2073,6 @@ public struct BackgroundShellWork: Codable, Sendable { case meta = "_meta" case kind case command - case terminal } public init( @@ -2084,8 +2081,7 @@ public struct BackgroundShellWork: Codable, Sendable { startedAt: String, meta: [String: AnyCodable]? = nil, kind: BackgroundWorkKind, - command: String, - terminal: String? = nil + command: String ) { self.id = id self.label = label @@ -2093,7 +2089,6 @@ public struct BackgroundShellWork: Codable, Sendable { self.meta = meta self.kind = kind self.command = command - self.terminal = terminal } } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 36e5c3193..43f60b329 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -151,10 +151,9 @@ subscriber can discover its chats' background work without reading transcripts. Each entry has a `kind`, a `label`, and a start time. Every entry is unfinished work; hosts remove entries when the work finishes rather than marking them done. A shell -entry adds its plain-text command and, when the host provides one, a terminal channel -for its output. The kind set is non-exhaustive: clients should keep entries of unknown -kinds and may render them from the common fields. Provider-specific details, such as -how a shell's lifetime is tied to its agent, belong in `_meta`. +entry adds its plain-text command. The kind set is non-exhaustive: clients should keep +entries of unknown kinds and may render them from the common fields. Provider-specific +details, such as how a shell's lifetime is tied to its agent, belong in `_meta`. The collection survives turn completion, cancellation, steering, and history truncation; those actions do not establish whether the work has stopped. Hosts must diff --git a/schema/actions.schema.json b/schema/actions.schema.json index ce3bd3d99..cbea923fb 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5151,10 +5151,6 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." - }, - "terminal": { - "$ref": "#/$defs/URI", - "description": "Terminal channel carrying this shell's output, when the host provides one." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index bd2bdbaae..348fbe3ff 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4261,10 +4261,6 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." - }, - "terminal": { - "$ref": "#/$defs/URI", - "description": "Terminal channel carrying this shell's output, when the host provides one." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index f12d4610b..2efbe98d5 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2690,10 +2690,6 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." - }, - "terminal": { - "$ref": "#/$defs/URI", - "description": "Terminal channel carrying this shell's output, when the host provides one." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index c91c12630..7b48b12d4 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2868,10 +2868,6 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." - }, - "terminal": { - "$ref": "#/$defs/URI", - "description": "Terminal channel carrying this shell's output, when the host provides one." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 34b3dee92..a0cb22163 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2601,10 +2601,6 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." - }, - "terminal": { - "$ref": "#/$defs/URI", - "description": "Terminal channel carrying this shell's output, when the host provides one." } }, "required": [ diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 6ad0bbc1a..5e38f99d4 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -212,8 +212,6 @@ export interface BackgroundShellWork extends BackgroundWorkBase { kind: BackgroundWorkKind.Shell; /** Command line, displayed as plain text. */ command: string; - /** Terminal channel carrying this shell's output, when the host provides one. */ - terminal?: URI; } /** From ec8f97c4f52f3b4ffa4db9dd1952d7480316705a Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 16:03:58 -0700 Subject: [PATCH 07/14] Link background shells to their terminal Addresses review thread https://github.com/microsoft/agent-host-protocol/pull/482#discussion_r4139082857. A shell entry now carries an optional terminal URI, so clients have something to open to check on the shell. It's optional because some hosts can't point at a live terminal for every shell yet. --- .../Generated/State.generated.cs | 7 +++++++ clients/go/ahptypes/state.generated.go | 5 +++++ .../agenthostprotocol/generated/State.generated.kt | 9 ++++++++- clients/rust/crates/ahp-types/src/state.rs | 6 ++++++ .../AgentHostProtocol/Generated/State.generated.swift | 10 +++++++++- docs/guide/state-model.md | 7 ++++--- schema/actions.schema.json | 4 ++++ schema/commands.schema.json | 4 ++++ schema/errors.schema.json | 4 ++++ schema/notifications.schema.json | 4 ++++ schema/state.schema.json | 4 ++++ types/channels-chat/state.ts | 7 +++++++ .../reducers/278-chat-backgroundworkset-replaces.json | 8 +++++--- 13 files changed, 71 insertions(+), 8 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index a45694aaf..957e3e4bb 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1207,6 +1207,13 @@ public sealed record BackgroundShellWork /// Command line, displayed as plain text. public required string Command { get; init; } + + /// Terminal carrying this shell's output. Hosts SHOULD set this whenever they + /// can show that output. Clients open it like + /// {@link ToolResultTerminalContent.resource}; `isPty` on its + /// {@link TerminalState} says whether the output is plain text. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Terminal { get; init; } } /// Full state for a single chat, loaded when a client subscribes to the chat's diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 81809d984..043538bf3 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1380,6 +1380,11 @@ type BackgroundShellWork struct { Kind BackgroundWorkKind `json:"kind"` // Command line, displayed as plain text. Command string `json:"command"` + // Terminal carrying this shell's output. Hosts SHOULD set this whenever they + // can show that output. Clients open it like + // {@link ToolResultTerminalContent.resource}; `isPty` on its + // {@link TerminalState} says whether the output is plain text. + Terminal *URI `json:"terminal,omitempty"` } // Immutable selected-text snapshot captured when a side chat is created. 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 a4f7608bc..aa2993db8 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 @@ -2000,7 +2000,14 @@ data class BackgroundShellWork( /** * Command line, displayed as plain text. */ - val command: String + val command: String, + /** + * Terminal carrying this shell's output. Hosts SHOULD set this whenever they + * can show that output. Clients open it like + * {@link ToolResultTerminalContent.resource}; `isPty` on its + * {@link TerminalState} says whether the output is plain text. + */ + val terminal: String? = null ) @Serializable diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 975b524bd..1db12d8ee 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2064,6 +2064,12 @@ pub struct BackgroundShellWork { pub meta: Option, /// Command line, displayed as plain text. pub command: String, + /// Terminal carrying this shell's output. Hosts SHOULD set this whenever they + /// can show that output. Clients open it like + /// {@link ToolResultTerminalContent.resource}; `isPty` on its + /// {@link TerminalState} says whether the output is plain text. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub terminal: Option, } /// Immutable selected-text snapshot captured when a side chat is created. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index e2e6c4b1a..2b3a858bc 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -2078,6 +2078,11 @@ public struct BackgroundShellWork: Codable, Sendable { public var kind: BackgroundWorkKind /// Command line, displayed as plain text. public var command: String + /// Terminal carrying this shell's output. Hosts SHOULD set this whenever they + /// can show that output. Clients open it like + /// {@link ToolResultTerminalContent.resource}; `isPty` on its + /// {@link TerminalState} says whether the output is plain text. + public var terminal: String? enum CodingKeys: String, CodingKey { case id @@ -2086,6 +2091,7 @@ public struct BackgroundShellWork: Codable, Sendable { case meta = "_meta" case kind case command + case terminal } public init( @@ -2094,7 +2100,8 @@ public struct BackgroundShellWork: Codable, Sendable { startedAt: String, meta: [String: AnyCodable]? = nil, kind: BackgroundWorkKind, - command: String + command: String, + terminal: String? = nil ) { self.id = id self.label = label @@ -2102,6 +2109,7 @@ public struct BackgroundShellWork: Codable, Sendable { self.meta = meta self.kind = kind self.command = command + self.terminal = terminal } } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 43f60b329..7c0b23fab 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -151,9 +151,10 @@ subscriber can discover its chats' background work without reading transcripts. Each entry has a `kind`, a `label`, and a start time. Every entry is unfinished work; hosts remove entries when the work finishes rather than marking them done. A shell -entry adds its plain-text command. The kind set is non-exhaustive: clients should keep -entries of unknown kinds and may render them from the common fields. Provider-specific -details, such as how a shell's lifetime is tied to its agent, belong in `_meta`. +entry adds its plain-text command and, when the host has one, the terminal carrying +its output. The kind set is non-exhaustive: clients should keep entries of unknown +kinds and may render them from the common fields. Provider-specific details, such as +how a shell's lifetime is tied to its agent, belong in `_meta`. The collection survives turn completion, cancellation, steering, and history truncation; those actions do not establish whether the work has stopped. Hosts must diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 0ea64b057..4097a78bd 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5163,6 +5163,10 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal carrying this shell's output. Hosts SHOULD set this whenever they\ncan show that output. Clients open it like\n{@link ToolResultTerminalContent.resource}; `isPty` on its\n{@link TerminalState} says whether the output is plain text." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index aa03b0791..83898aedf 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4269,6 +4269,10 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal carrying this shell's output. Hosts SHOULD set this whenever they\ncan show that output. Clients open it like\n{@link ToolResultTerminalContent.resource}; `isPty` on its\n{@link TerminalState} says whether the output is plain text." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 0892f8150..6d45e4ccb 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2698,6 +2698,10 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal carrying this shell's output. Hosts SHOULD set this whenever they\ncan show that output. Clients open it like\n{@link ToolResultTerminalContent.resource}; `isPty` on its\n{@link TerminalState} says whether the output is plain text." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 2b24bedd3..0cd64ffab 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2876,6 +2876,10 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal carrying this shell's output. Hosts SHOULD set this whenever they\ncan show that output. Clients open it like\n{@link ToolResultTerminalContent.resource}; `isPty` on its\n{@link TerminalState} says whether the output is plain text." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 2a8bae7fc..a47519e5d 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2609,6 +2609,10 @@ "command": { "type": "string", "description": "Command line, displayed as plain text." + }, + "terminal": { + "$ref": "#/$defs/URI", + "description": "Terminal carrying this shell's output. Hosts SHOULD set this whenever they\ncan show that output. Clients open it like\n{@link ToolResultTerminalContent.resource}; `isPty` on its\n{@link TerminalState} says whether the output is plain text." } }, "required": [ diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 73894331d..e96b2f53e 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -229,6 +229,13 @@ export interface BackgroundShellWork extends BackgroundWorkBase { kind: BackgroundWorkKind.Shell; /** Command line, displayed as plain text. */ command: string; + /** + * Terminal carrying this shell's output. Hosts SHOULD set this whenever they + * can show that output. Clients open it like + * {@link ToolResultTerminalContent.resource}; `isPty` on its + * {@link TerminalState} says whether the output is plain text. + */ + terminal?: URI; } /** diff --git a/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json b/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json index 123519402..69fb9e26c 100644 --- a/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json +++ b/types/test-cases/reducers/278-chat-backgroundworkset-replaces.json @@ -1,5 +1,5 @@ { - "description": "setting background work replaces by ID without duplication", + "description": "setting background work replaces by ID without duplication, adding its terminal", "reducer": "chat", "initial": { "resource": "ahp-chat:/session/main", @@ -25,7 +25,8 @@ "id": "shell-1", "label": "Run unit tests", "command": "npm test", - "startedAt": "2026-09-25T00:00:00.000Z" + "startedAt": "2026-09-25T00:00:00.000Z", + "terminal": "agenthost:/terminal/1" } } ], @@ -41,7 +42,8 @@ "id": "shell-1", "label": "Run unit tests", "command": "npm test", - "startedAt": "2026-09-25T00:00:00.000Z" + "startedAt": "2026-09-25T00:00:00.000Z", + "terminal": "agenthost:/terminal/1" } ] } From 5e07a483d053777c47bc194a7c93cc8faf3808b7 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 16:05:01 -0700 Subject: [PATCH 08/14] Restore the Swift session input request doc comment Adding the background work id helper split sessionInputRequestID from its doc comment. Move the comment back and give the new helper its own. --- .../AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index d486a6f36..f631987fb 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -72,7 +72,7 @@ private func refineToolCallContributor(_ current: ToolCallContributor?, _ next: return next } -/// Extracts the stable `id` of a session input request, or `nil` for unknown variants. +/// Extracts the stable `id` of background work, including kinds from newer hosts. private func backgroundWorkID(_ w: BackgroundWork) -> String? { switch w { case .shell(let x): return x.id @@ -81,6 +81,7 @@ private func backgroundWorkID(_ w: BackgroundWork) -> String? { } } +/// Extracts the stable `id` of a session input request, or `nil` for unknown variants. private func sessionInputRequestID(_ r: SessionInputRequest) -> String? { switch r { case .chatInput(let x): return x.id From 8d71bd1930b0f747534bba00aeebb6da93afe56a Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 16:06:35 -0700 Subject: [PATCH 09/14] Add subagents to background work Addresses review thread https://github.com/microsoft/agent-host-protocol/pull/482#discussion_r4139084415. A background subagent is now its own kind, pointing at the subagent's chat instead of repeating its state. That's the same chat the spawning tool call's subagent content points to. --- .../JsonSerializerContext.generated.cs | 1 + .../Generated/State.generated.cs | 36 +++++++++++- .../dotnet/src/AgentHostProtocol/Reducers.cs | 1 + clients/go/ahp/reducers.go | 2 + clients/go/ahptypes/state.generated.go | 35 +++++++++++- .../microsoft/agenthostprotocol/Reducers.kt | 1 + .../generated/State.generated.kt | 43 +++++++++++++- clients/rust/crates/ahp-types/src/state.rs | 32 ++++++++++- clients/rust/crates/ahp/src/reducers.rs | 1 + .../Generated/State.generated.swift | 56 ++++++++++++++++++- .../Sources/AgentHostProtocol/Reducers.swift | 1 + .../20260929-chat-background-work.json | 2 +- docs/guide/state-model.md | 5 +- schema/actions.schema.json | 52 +++++++++++++++-- schema/commands.schema.json | 52 +++++++++++++++-- schema/errors.schema.json | 52 +++++++++++++++-- schema/notifications.schema.json | 52 +++++++++++++++-- schema/state.schema.json | 52 +++++++++++++++-- scripts/generate-csharp.ts | 2 + scripts/generate-go.ts | 2 + scripts/generate-kotlin.ts | 3 +- scripts/generate-rust.ts | 2 + scripts/generate-swift.ts | 3 +- types/channels-chat/state.ts | 24 +++++++- .../277-chat-backgroundworkset-adds.json | 19 ++++++- ...79-chat-backgroundworkremoved-removes.json | 19 ++++++- 26 files changed, 508 insertions(+), 42 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index b0a926f7b..97dda1c5e 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -77,6 +77,7 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(AutomationTriggerKind))] [JsonSerializable(typeof(AutomationUpdateRequestedAction))] [JsonSerializable(typeof(BackgroundShellWork))] +[JsonSerializable(typeof(BackgroundSubagentWork))] [JsonSerializable(typeof(BackgroundWork))] [JsonSerializable(typeof(BackgroundWorkKind))] [JsonSerializable(typeof(Changeset))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 957e3e4bb..54fe8daf2 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -467,6 +467,9 @@ public enum BackgroundWorkKind /// A shell command that continues after its initiating tool call returns. [WireValue("shell")] Shell, + /// A subagent running in the background. + [WireValue("subagent")] + Subagent, } /// Discriminant for the {@link McpServerState} union. @@ -1192,7 +1195,7 @@ public sealed record BackgroundShellWork /// convention. public required string Id { get; init; } - /// Human-readable label, such as the command's purpose. + /// Human-readable label, such as the command's purpose or the subagent's name. public required string Label { get; init; } /// ISO 8601 timestamp when the work started. @@ -1216,6 +1219,34 @@ public sealed record BackgroundShellWork public string? Terminal { get; init; } } +/// A subagent running in the background. Its own state lives in its chat. +public sealed record BackgroundSubagentWork +{ + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + public required string Id { get; init; } + + /// Human-readable label, such as the command's purpose or the subagent's name. + public required string Label { get; init; } + + /// ISO 8601 timestamp when the work started. + public required string StartedAt { get; init; } + + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + [JsonPropertyName("_meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Meta { get; init; } + + public BackgroundWorkKind Kind { get; init; } + + /// The subagent's chat: the same chat the spawning tool call's + /// {@link ToolResultSubagentContent.resource} points to. + public required string Chat { get; init; } +} + /// Full state for a single chat, loaded when a client subscribes to the chat's /// URI. /// @@ -1243,7 +1274,7 @@ public sealed class ChatState public string? Activity { get; set; } /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells. Independent of turn state. + /// finishes, such as background shells and subagents. Independent of turn state. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? BackgroundWork { get; set; } @@ -6245,6 +6276,7 @@ public BackgroundWorkConverter() variants: new Dictionary { ["shell"] = typeof(BackgroundShellWork), + ["subagent"] = typeof(BackgroundSubagentWork), }, allowUnknown: true) { diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs index b465adf59..f6bba5832 100644 --- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs +++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs @@ -94,6 +94,7 @@ private static SessionStatus WithInputNeededStatus(SessionStatus status, List work.Value switch { BackgroundShellWork v => v.Id, + BackgroundSubagentWork v => v.Id, JsonElement e when e.TryGetProperty("id", out JsonElement id) => id.GetString() ?? string.Empty, _ => string.Empty, }; diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 9f60968cf..ecfda5a82 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -327,6 +327,8 @@ func backgroundWorkID(w ahptypes.BackgroundWork) (string, bool) { switch v := w.Value.(type) { case *ahptypes.BackgroundShellWork: return v.Id, true + case *ahptypes.BackgroundSubagentWork: + return v.Id, true case *ahptypes.BackgroundWorkUnknown: // Kinds from newer hosts still carry the common `id`, so they can be replaced and removed. var common struct { diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 043538bf3..e49087c8e 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -368,6 +368,8 @@ type BackgroundWorkKind string const ( // A shell command that continues after its initiating tool call returns. BackgroundWorkKindShell BackgroundWorkKind = "shell" + // A subagent running in the background. + BackgroundWorkKindSubagent BackgroundWorkKind = "subagent" ) // Discriminant for the {@link McpServerState} union. @@ -1262,7 +1264,7 @@ type ChatState struct { // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` // Work running outside the current turn that will resume this chat when it - // finishes, such as background shells. Independent of turn state. + // finishes, such as background shells and subagents. Independent of turn state. BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` @@ -1371,7 +1373,7 @@ type BackgroundShellWork struct { // the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert // convention. Id string `json:"id"` - // Human-readable label, such as the command's purpose. + // Human-readable label, such as the command's purpose or the subagent's name. Label string `json:"label"` // ISO 8601 timestamp when the work started. StartedAt string `json:"startedAt"` @@ -1387,6 +1389,26 @@ type BackgroundShellWork struct { Terminal *URI `json:"terminal,omitempty"` } +// A subagent running in the background. Its own state lives in its chat. +type BackgroundSubagentWork struct { + // Identifier of this entry, unique within the owning chat across all kinds. + // The host derives it however it likes (for example from the kind plus the + // agent's own task id); consumers MUST treat it as opaque. It is the key for + // the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + // convention. + Id string `json:"id"` + // Human-readable label, such as the command's purpose or the subagent's name. + Label string `json:"label"` + // ISO 8601 timestamp when the work started. + StartedAt string `json:"startedAt"` + // Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Kind BackgroundWorkKind `json:"kind"` + // The subagent's chat: the same chat the spawning tool call's + // {@link ToolResultSubagentContent.resource} points to. + Chat URI `json:"chat"` +} + // Immutable selected-text snapshot captured when a side chat is created. // // The host records this exact text when it accepts `createChat`; later changes @@ -5672,7 +5694,8 @@ type BackgroundWork struct { // concrete variant of BackgroundWork. type isBackgroundWork interface{ isBackgroundWork() } -func (*BackgroundShellWork) isBackgroundWork() {} +func (*BackgroundShellWork) isBackgroundWork() {} +func (*BackgroundSubagentWork) isBackgroundWork() {} // BackgroundWorkUnknown carries an unrecognized BackgroundWork 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 BackgroundWorkUnknown struct { @@ -5694,6 +5717,12 @@ func (u *BackgroundWork) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "subagent": + var value BackgroundSubagentWork + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value default: raw := make(json.RawMessage, len(data)) copy(raw, data) 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 10b6b0b0c..c4562d830 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -256,6 +256,7 @@ private fun customizationId(c: Customization): String? = when (c) { private fun backgroundWorkId(w: BackgroundWork): String? = when (w) { is BackgroundWorkShell -> w.value.id + is BackgroundWorkSubagent -> w.value.id // Kinds from newer hosts still carry the common `id`, so they can be replaced and removed. is BackgroundWorkUnknown -> (w.raw["id"] as? JsonPrimitive)?.takeIf { it.isString }?.content } 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 aa2993db8..d2725f113 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 @@ -780,6 +780,10 @@ value class BackgroundWorkKind(val rawValue: String) { * A shell command that continues after its initiating tool call returns. */ val SHELL: BackgroundWorkKind = BackgroundWorkKind("shell") + /** + * A subagent running in the background. + */ + val SUBAGENT: BackgroundWorkKind = BackgroundWorkKind("subagent") } } @@ -1637,7 +1641,7 @@ data class ChatState( val activity: String? = null, /** * Work running outside the current turn that will resume this chat when it - * finishes, such as background shells. Independent of turn state. + * finishes, such as background shells and subagents. Independent of turn state. */ val backgroundWork: List? = null, /** @@ -1984,7 +1988,7 @@ data class BackgroundShellWork( */ val id: String, /** - * Human-readable label, such as the command's purpose. + * Human-readable label, such as the command's purpose or the subagent's name. */ val label: String, /** @@ -2010,6 +2014,37 @@ data class BackgroundShellWork( val terminal: String? = null ) +@Serializable +data class BackgroundSubagentWork( + /** + * Identifier of this entry, unique within the owning chat across all kinds. + * The host derives it however it likes (for example from the kind plus the + * agent's own task id); consumers MUST treat it as opaque. It is the key for + * the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + * convention. + */ + val id: String, + /** + * Human-readable label, such as the command's purpose or the subagent's name. + */ + val label: String, + /** + * ISO 8601 timestamp when the work started. + */ + val startedAt: String, + /** + * Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + */ + @SerialName("_meta") + val meta: Map? = null, + val kind: BackgroundWorkKind, + /** + * The subagent's chat: the same chat the spawning tool call's + * {@link ToolResultSubagentContent.resource} points to. + */ + val chat: String +) + @Serializable data class SessionChatInputRequest( /** @@ -6951,6 +6986,8 @@ sealed interface BackgroundWork @JvmInline value class BackgroundWorkShell(val value: BackgroundShellWork) : BackgroundWork +@JvmInline +value class BackgroundWorkSubagent(val value: BackgroundSubagentWork) : BackgroundWork /** * Forward-compat catch-all for unknown BackgroundWork discriminators. * @@ -6976,6 +7013,7 @@ internal object BackgroundWorkSerializer : KSerializer { ?: return BackgroundWorkUnknown(obj) return when (discriminant) { "shell" -> BackgroundWorkShell(input.json.decodeFromJsonElement(BackgroundShellWork.serializer(), element)) + "subagent" -> BackgroundWorkSubagent(input.json.decodeFromJsonElement(BackgroundSubagentWork.serializer(), element)) else -> BackgroundWorkUnknown(obj) } } @@ -6985,6 +7023,7 @@ internal object BackgroundWorkSerializer : KSerializer { ?: error("BackgroundWork can only be serialized to JSON") val element: JsonElement = when (value) { is BackgroundWorkShell -> output.json.encodeToJsonElement(BackgroundShellWork.serializer(), value.value) + is BackgroundWorkSubagent -> output.json.encodeToJsonElement(BackgroundSubagentWork.serializer(), value.value) is BackgroundWorkUnknown -> value.raw } output.encodeJsonElement(element) diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 1db12d8ee..4a080645d 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1002,6 +1002,8 @@ pub enum TerminalLifecycleStatus { pub enum BackgroundWorkKind { /// A shell command that continues after its initiating tool call returns. Shell, + /// A subagent running in the background. + Subagent, /// Unknown raw value from a newer protocol version, preserved verbatim. Unknown(String), } @@ -1013,6 +1015,7 @@ impl serde::Serialize for BackgroundWorkKind { { match self { Self::Shell => serializer.serialize_str("shell"), + Self::Subagent => serializer.serialize_str("subagent"), Self::Unknown(value) => serializer.serialize_str(value), } } @@ -1026,6 +1029,7 @@ impl<'de> serde::Deserialize<'de> for BackgroundWorkKind { let raw = ::deserialize(deserializer)?; Ok(match raw.as_str() { "shell" => Self::Shell, + "subagent" => Self::Subagent, _ => Self::Unknown(raw), }) } @@ -1924,7 +1928,7 @@ pub struct ChatState { #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells. Independent of turn state. + /// finishes, such as background shells and subagents. Independent of turn state. #[serde(default, skip_serializing_if = "Option::is_none")] pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) @@ -2055,7 +2059,7 @@ pub struct BackgroundShellWork { /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert /// convention. pub id: String, - /// Human-readable label, such as the command's purpose. + /// Human-readable label, such as the command's purpose or the subagent's name. pub label: String, /// ISO 8601 timestamp when the work started. pub started_at: String, @@ -2072,6 +2076,28 @@ pub struct BackgroundShellWork { pub terminal: Option, } +/// A subagent running in the background. Its own state lives in its chat. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BackgroundSubagentWork { + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + pub id: String, + /// Human-readable label, such as the command's purpose or the subagent's name. + pub label: String, + /// ISO 8601 timestamp when the work started. + pub started_at: String, + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// The subagent's chat: the same chat the spawning tool call's + /// {@link ToolResultSubagentContent.resource} points to. + pub chat: Uri, +} + /// Immutable selected-text snapshot captured when a side chat is created. /// /// The host records this exact text when it accepts `createChat`; later changes @@ -6263,6 +6289,8 @@ pub enum SessionInputRequest { pub enum BackgroundWork { #[serde(rename = "shell")] Shell(BackgroundShellWork), + #[serde(rename = "subagent")] + Subagent(BackgroundSubagentWork), /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. /// Reducers treat this as a no-op. #[serde(untagged)] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 6e96cd207..66a5d76ef 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -486,6 +486,7 @@ fn session_input_request_id(r: &SessionInputRequest) -> Option<&str> { fn background_work_id(w: &BackgroundWork) -> Option<&str> { match w { BackgroundWork::Shell(x) => Some(x.id.as_str()), + BackgroundWork::Subagent(x) => Some(x.id.as_str()), BackgroundWork::Unknown(v) => v.get("id").and_then(serde_json::Value::as_str), } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 2b3a858bc..bc911e3f1 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -767,6 +767,8 @@ public enum TerminalLifecycleStatus: String, Codable, Sendable { public enum BackgroundWorkKind: Codable, Sendable, Equatable { /// A shell command that continues after its initiating tool call returns. case shell + /// A subagent running in the background. + case subagent /// Unknown raw value from a newer protocol version, preserved verbatim. case unknown(String) @@ -775,6 +777,7 @@ public enum BackgroundWorkKind: Codable, Sendable, Equatable { let raw = try container.decode(String.self) switch raw { case "shell": self = .shell + case "subagent": self = .subagent default: self = .unknown(raw) } } @@ -783,6 +786,7 @@ public enum BackgroundWorkKind: Codable, Sendable, Equatable { var container = encoder.singleValueContainer() switch self { case .shell: try container.encode("shell") + case .subagent: try container.encode("subagent") case .unknown(let raw): try container.encode(raw) } } @@ -1660,7 +1664,7 @@ public struct ChatState: Codable, Sendable { /// Human-readable description of what the chat is currently doing public var activity: String? /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells. Independent of turn state. + /// finishes, such as background shells and subagents. Independent of turn state. public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String @@ -2069,7 +2073,7 @@ public struct BackgroundShellWork: Codable, Sendable { /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert /// convention. public var id: String - /// Human-readable label, such as the command's purpose. + /// Human-readable label, such as the command's purpose or the subagent's name. public var label: String /// ISO 8601 timestamp when the work started. public var startedAt: String @@ -2113,6 +2117,50 @@ public struct BackgroundShellWork: Codable, Sendable { } } +public struct BackgroundSubagentWork: Codable, Sendable { + /// Identifier of this entry, unique within the owning chat across all kinds. + /// The host derives it however it likes (for example from the kind plus the + /// agent's own task id); consumers MUST treat it as opaque. It is the key for + /// the `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert + /// convention. + public var id: String + /// Human-readable label, such as the command's purpose or the subagent's name. + public var label: String + /// ISO 8601 timestamp when the work started. + public var startedAt: String + /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + public var meta: [String: AnyCodable]? + public var kind: BackgroundWorkKind + /// The subagent's chat: the same chat the spawning tool call's + /// {@link ToolResultSubagentContent.resource} points to. + public var chat: String + + enum CodingKeys: String, CodingKey { + case id + case label + case startedAt + case meta = "_meta" + case kind + case chat + } + + public init( + id: String, + label: String, + startedAt: String, + meta: [String: AnyCodable]? = nil, + kind: BackgroundWorkKind, + chat: String + ) { + self.id = id + self.label = label + self.startedAt = startedAt + self.meta = meta + self.kind = kind + self.chat = chat + } +} + public struct SessionChatInputRequest: Codable, Sendable { /// Stable key for this entry, unique within the session's /// {@link SessionState.inputNeeded} list. The host derives it however it likes @@ -7800,6 +7848,7 @@ public enum SessionInputRequest: Codable, Sendable { } public enum BackgroundWork: Codable, Sendable { case shell(BackgroundShellWork) + case subagent(BackgroundSubagentWork) /// Unknown or future discriminant; the raw payload is preserved /// and re-encoded verbatim for forward-compatibility. case unknown(AnyCodable) @@ -7817,6 +7866,8 @@ public enum BackgroundWork: Codable, Sendable { switch discriminant { case "shell": self = .shell(try BackgroundShellWork(from: decoder)) + case "subagent": + self = .subagent(try BackgroundSubagentWork(from: decoder)) default: self = .unknown(try AnyCodable(from: decoder)) } @@ -7825,6 +7876,7 @@ public enum BackgroundWork: Codable, Sendable { public func encode(to encoder: Encoder) throws { switch self { case .shell(let value): try value.encode(to: encoder) + case .subagent(let value): try value.encode(to: encoder) case .unknown(let value): try value.encode(to: encoder) } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index f631987fb..fa9efaafe 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -76,6 +76,7 @@ private func refineToolCallContributor(_ current: ToolCallContributor?, _ next: private func backgroundWorkID(_ w: BackgroundWork) -> String? { switch w { case .shell(let x): return x.id + case .subagent(let x): return x.id // Kinds from newer hosts still carry the common `id`, so they can be replaced and removed. case .unknown(let raw): return (raw.value as? [String: Any])?["id"] as? String } diff --git a/docs/.changes/20260929-chat-background-work.json b/docs/.changes/20260929-chat-background-work.json index 930870ff2..3a534a88f 100644 --- a/docs/.changes/20260929-chat-background-work.json +++ b/docs/.changes/20260929-chat-background-work.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Expose chat-owned background work, starting with background shells, in chat state and session chat summaries, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime." + "message": "Expose chat-owned background work (background shells and subagents) in chat state and session chat summaries, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime." } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 7c0b23fab..7df932061 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -142,7 +142,7 @@ For example, `(status & SessionStatus.InProgress) !== 0` is true for both `InPro Subscribable on a [Chat Channel](/specification/chat-channel) at `ahp-chat:/`. A session is a catalog of chats (`SessionState.chats`); each chat carries the per-conversation state — the turn history, the active turn and its streaming response parts (including live input requests), tool calls, steering/queued messages, and the user's in-progress draft. A session starts with a default chat (`SessionState.defaultChat`); hosts advertising the `multipleChats` capability let clients open more via `createChat`. `backgroundWork` lists work running outside the current turn that will resume the -chat when it finishes, starting with background shells. +chat when it finishes, such as background shells and subagents. Hosts publish complete entries with `chat/backgroundWorkSet` and remove them with `chat/backgroundWorkRemoved` when they finish or are no longer tracked. Entry IDs are opaque, unique within a chat across all kinds, and scoped to that chat. Hosts mirror @@ -152,7 +152,8 @@ subscriber can discover its chats' background work without reading transcripts. Each entry has a `kind`, a `label`, and a start time. Every entry is unfinished work; hosts remove entries when the work finishes rather than marking them done. A shell entry adds its plain-text command and, when the host has one, the terminal carrying -its output. The kind set is non-exhaustive: clients should keep entries of unknown +its output. A subagent entry points to the subagent's own chat instead of repeating +its state. The kind set is non-exhaustive: clients should keep entries of unknown kinds and may render them from the common fields. Provider-specific details, such as how a shell's lifetime is tied to its agent, belong in `_meta`. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 4097a78bd..44e7616a6 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -4974,7 +4974,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -5118,7 +5118,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -5146,7 +5146,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -5177,6 +5177,43 @@ "command" ] }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat: the same chat the spawning tool call's\n{@link ToolResultSubagentContent.resource} points to." + } + }, + "required": [ + "id", + "label", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -8517,7 +8554,14 @@ "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, "BackgroundWork": { - "$ref": "#/$defs/BackgroundShellWork", + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "ChatOrigin": { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 83898aedf..aac4047b4 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4080,7 +4080,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -4224,7 +4224,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -4252,7 +4252,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -4283,6 +4283,43 @@ "command" ] }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat: the same chat the spawning tool call's\n{@link ToolResultSubagentContent.resource} points to." + } + }, + "required": [ + "id", + "label", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -10635,7 +10672,14 @@ "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, "BackgroundWork": { - "$ref": "#/$defs/BackgroundShellWork", + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "ChatInputQuestion": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 6d45e4ccb..6d521a004 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2509,7 +2509,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -2653,7 +2653,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -2681,7 +2681,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -2712,6 +2712,43 @@ "command" ] }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat: the same chat the spawning tool call's\n{@link ToolResultSubagentContent.resource} points to." + } + }, + "required": [ + "id", + "label", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -7832,7 +7869,14 @@ "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, "BackgroundWork": { - "$ref": "#/$defs/BackgroundShellWork", + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "ChatInputQuestion": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 0cd64ffab..175b92832 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2687,7 +2687,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -2831,7 +2831,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -2859,7 +2859,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -2890,6 +2890,43 @@ "command" ] }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat: the same chat the spawning tool call's\n{@link ToolResultSubagentContent.resource} points to." + } + }, + "required": [ + "id", + "label", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -6358,7 +6395,14 @@ "description": "Why an MCP server is currently in the {@link McpServerStatus.AuthRequired}\nstate. Mirrors the three failure modes defined by the\n[MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.md)." }, "BackgroundWork": { - "$ref": "#/$defs/BackgroundShellWork", + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "ChatInputQuestion": { diff --git a/schema/state.schema.json b/schema/state.schema.json index a47519e5d..05d41baed 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2420,7 +2420,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells. Independent of turn state." + "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." }, "modifiedAt": { "type": "string", @@ -2564,7 +2564,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -2592,7 +2592,7 @@ }, "label": { "type": "string", - "description": "Human-readable label, such as the command's purpose." + "description": "Human-readable label, such as the command's purpose or the subagent's name." }, "startedAt": { "type": "string", @@ -2623,6 +2623,43 @@ "command" ] }, + "BackgroundSubagentWork": { + "type": "object", + "description": "A subagent running in the background. Its own state lives in its chat.", + "properties": { + "id": { + "type": "string", + "description": "Identifier of this entry, unique within the owning chat across all kinds.\nThe host derives it however it likes (for example from the kind plus the\nagent's own task id); consumers MUST treat it as opaque. It is the key for\nthe `chat/backgroundWorkSet` / `chat/backgroundWorkRemoved` upsert\nconvention." + }, + "label": { + "type": "string", + "description": "Human-readable label, such as the command's purpose or the subagent's name." + }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the work started." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + }, + "kind": { + "const": "subagent" + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The subagent's chat: the same chat the spawning tool call's\n{@link ToolResultSubagentContent.resource} points to." + } + }, + "required": [ + "id", + "label", + "startedAt", + "kind", + "chat" + ] + }, "SideChatSelection": { "type": "object", "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", @@ -5963,7 +6000,14 @@ "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." }, "BackgroundWork": { - "$ref": "#/$defs/BackgroundShellWork", + "oneOf": [ + { + "$ref": "#/$defs/BackgroundShellWork" + }, + { + "$ref": "#/$defs/BackgroundSubagentWork" + } + ], "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." }, "ChatOrigin": { diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index d85525d50..55bcd135b 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -669,6 +669,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin { name: 'PendingMessage' }, { name: 'ChatSummary', mutable: true }, { name: 'BackgroundShellWork' }, + { name: 'BackgroundSubagentWork' }, { name: 'ChatState', mutable: true }, { name: 'ChatInputOption' }, { name: 'ChatInputTextQuestion' }, @@ -1165,6 +1166,7 @@ const BACKGROUND_WORK_UNION: UnionConfig = { doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, + { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 5e6638250..d76ba5d35 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -761,6 +761,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'ChatState' }, { name: 'ChatSummary' }, { name: 'BackgroundShellWork' }, + { name: 'BackgroundSubagentWork' }, { name: 'SideChatSelection' }, { name: 'PendingMessage' }, { name: 'ProjectInfo' }, @@ -1137,6 +1138,7 @@ const BACKGROUND_WORK_UNION: UnionConfig = { doc: 'BackgroundWork is work running outside the current turn that will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, + { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index f26deadeb..28c364e70 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -991,7 +991,7 @@ const STATE_STRUCTS = [ 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', - 'BackgroundShellWork', + 'BackgroundShellWork', 'BackgroundSubagentWork', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionToolAuthenticationRequest', 'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', @@ -1331,6 +1331,7 @@ const BACKGROUND_WORK_UNION: UnionConfig = { discriminantField: 'kind', variants: [ { caseName: 'Shell', structName: 'BackgroundShellWork', discriminantValue: 'shell' }, + { caseName: 'Subagent', structName: 'BackgroundSubagentWork', discriminantValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 2176a1f43..1b22154f6 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -811,6 +811,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'ChatState' }, { name: 'ChatSummary' }, { name: 'BackgroundShellWork', omitDiscriminants: true }, + { name: 'BackgroundSubagentWork', omitDiscriminants: true }, { name: 'SideChatSelection' }, { name: 'SessionState' }, { name: 'SessionActiveClient' }, @@ -1200,6 +1201,7 @@ const BACKGROUND_WORK_UNION: UnionConfig = { doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, + { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, ], unknown: true, }; diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index fa2a0d273..dccc7ceb7 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -696,7 +696,7 @@ const STATE_STRUCTS = [ 'MultipleWorkingDirectoriesCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', - 'BackgroundShellWork', + 'BackgroundShellWork', 'BackgroundSubagentWork', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', 'SessionToolAuthenticationRequest', 'SessionSummary', 'SessionChatSummary', 'ChangesSummary', 'ProjectInfo', 'SessionConfigState', 'Turn', 'ActiveTurn', 'Message', @@ -988,6 +988,7 @@ const BACKGROUND_WORK_UNION: UnionConfig = { allowUnknown: true, variants: [ { caseName: 'shell', structName: 'BackgroundShellWork', discriminantValue: 'shell' }, + { caseName: 'subagent', structName: 'BackgroundSubagentWork', discriminantValue: 'subagent' }, ], }; diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index e96b2f53e..3e4557f5f 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -56,7 +56,7 @@ export interface ChatState { activity?: string; /** * Work running outside the current turn that will resume this chat when it - * finishes, such as background shells. Independent of turn state. + * finishes, such as background shells and subagents. Independent of turn state. */ backgroundWork?: BackgroundWork[]; /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -196,6 +196,8 @@ export interface ChatSummary { export const enum BackgroundWorkKind { /** A shell command that continues after its initiating tool call returns. */ Shell = 'shell', + /** A subagent running in the background. */ + Subagent = 'subagent', } /** @@ -212,7 +214,7 @@ interface BackgroundWorkBase { * convention. */ id: string; - /** Human-readable label, such as the command's purpose. */ + /** Human-readable label, such as the command's purpose or the subagent's name. */ label: string; /** ISO 8601 timestamp when the work started. */ startedAt: string; @@ -238,6 +240,20 @@ export interface BackgroundShellWork extends BackgroundWorkBase { terminal?: URI; } +/** + * A subagent running in the background. Its own state lives in its chat. + * + * @category Background Work + */ +export interface BackgroundSubagentWork extends BackgroundWorkBase { + kind: BackgroundWorkKind.Subagent; + /** + * The subagent's chat: the same chat the spawning tool call's + * {@link ToolResultSubagentContent.resource} points to. + */ + chat: URI; +} + /** * Work running outside the current turn that will resume the owning chat when * it finishes. Clients that don't recognize a `kind` should keep the entry and @@ -245,7 +261,9 @@ export interface BackgroundShellWork extends BackgroundWorkBase { * * @category Background Work */ -export type BackgroundWork = BackgroundShellWork; +export type BackgroundWork = + | BackgroundShellWork + | BackgroundSubagentWork; /** * Discriminant for {@link ChatOrigin} — how a chat came into existence. diff --git a/types/test-cases/reducers/277-chat-backgroundworkset-adds.json b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json index 19b95d973..04a41dfd5 100644 --- a/types/test-cases/reducers/277-chat-backgroundworkset-adds.json +++ b/types/test-cases/reducers/277-chat-backgroundworkset-adds.json @@ -1,5 +1,5 @@ { - "description": "setting background work appends a new entry", + "description": "setting background work appends shells and subagents in order", "reducer": "chat", "initial": { "resource": "ahp-chat:/session/main", @@ -18,6 +18,16 @@ "command": "npm test", "startedAt": "2026-09-25T00:00:00.000Z" } + }, + { + "type": "chat/backgroundWorkSet", + "work": { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" + } } ], "expected": { @@ -33,6 +43,13 @@ "label": "Run tests", "command": "npm test", "startedAt": "2026-09-25T00:00:00.000Z" + }, + { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" } ] } diff --git a/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json index 3193e0682..2414f3cc3 100644 --- a/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json +++ b/types/test-cases/reducers/279-chat-backgroundworkremoved-removes.json @@ -1,5 +1,5 @@ { - "description": "finished background work is removed without changing chat status", + "description": "removing finished background work keeps other entries and chat status", "reducer": "chat", "initial": { "resource": "ahp-chat:/session/main", @@ -14,6 +14,13 @@ "label": "Run tests", "command": "npm test", "startedAt": "2026-09-25T00:00:00.000Z" + }, + { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" } ] }, @@ -33,6 +40,14 @@ "status": 1, "modifiedAt": "2026-09-25T00:00:00.000Z", "turns": [], - "backgroundWork": [] + "backgroundWork": [ + { + "kind": "subagent", + "id": "subagent-1", + "label": "Review changes", + "startedAt": "2026-09-25T00:01:00.000Z", + "chat": "ahp-chat:/session/subagent-tool-1" + } + ] } } From 413fa1b363a53692aed7d67ef091ad1862e3a695 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 16:39:25 -0700 Subject: [PATCH 10/14] Clarify what background work covers Addresses review thread https://github.com/microsoft/agent-host-protocol/pull/482#discussion_r4139290201. Attached shells can keep the turn that started them open, so "outside the current turn" was wrong. Describe background work as work that keeps running after its tool call returns, and document that shells can be attached or detached, with that detail kept in the shell's _meta instead of the shared fields. --- .../Generated/State.generated.cs | 17 +++++++++++------ clients/go/ahptypes/state.generated.go | 17 +++++++++++------ .../generated/State.generated.kt | 10 ++++++---- clients/rust/crates/ahp-types/src/state.rs | 17 +++++++++++------ .../Generated/State.generated.swift | 10 ++++++---- docs/guide/state-model.md | 15 +++++++++------ schema/actions.schema.json | 12 ++++++------ schema/commands.schema.json | 12 ++++++------ schema/errors.schema.json | 12 ++++++------ schema/notifications.schema.json | 12 ++++++------ schema/state.schema.json | 12 ++++++------ scripts/generate-csharp.ts | 2 +- scripts/generate-go.ts | 2 +- scripts/generate-rust.ts | 2 +- types/channels-chat/state.ts | 19 ++++++++++++------- 15 files changed, 99 insertions(+), 72 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 54fe8daf2..08d455a25 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1185,7 +1185,10 @@ public sealed class ChatSummary public List? WorkingDirectories { get; set; } } -/// A shell command continuing outside its initiating tool call. +/// A shell command continuing outside its initiating tool call. Covers shells +/// tied to the agent's lifetime (attached) and shells that outlive it +/// (detached). Whether a shell is attached is provider-specific and goes in its +/// `_meta`. public sealed record BackgroundShellWork { /// Identifier of this entry, unique within the owning chat across all kinds. @@ -1201,7 +1204,7 @@ public sealed record BackgroundShellWork /// ISO 8601 timestamp when the work started. public required string StartedAt { get; init; } - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + /// Provider-specific metadata. [JsonPropertyName("_meta")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Meta { get; init; } @@ -1235,7 +1238,7 @@ public sealed record BackgroundSubagentWork /// ISO 8601 timestamp when the work started. public required string StartedAt { get; init; } - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + /// Provider-specific metadata. [JsonPropertyName("_meta")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Meta { get; init; } @@ -1273,8 +1276,10 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; set; } - /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells and subagents. Independent of turn state. + /// Work that keeps running after the tool call that started it returns and + /// will resume this chat when it finishes, such as background shells and + /// subagents. Entries stay listed whether or not the turn that started them is + /// still open. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? BackgroundWork { get; set; } @@ -6256,7 +6261,7 @@ public SessionInputRequestConverter() } } -/// Work running outside the current turn that will resume the owning chat when it finishes. +/// Work that keeps running after the tool call that started it returns and will resume the owning chat when it finishes. [JsonConverter(typeof(BackgroundWorkConverter))] public sealed class BackgroundWork : AhpUnion { diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index e49087c8e..79e760aa8 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1263,8 +1263,10 @@ type ChatState struct { Status SessionStatus `json:"status"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` - // Work running outside the current turn that will resume this chat when it - // finishes, such as background shells and subagents. Independent of turn state. + // Work that keeps running after the tool call that started it returns and + // will resume this chat when it finishes, such as background shells and + // subagents. Entries stay listed whether or not the turn that started them is + // still open. BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` @@ -1365,7 +1367,10 @@ type ChatSummary struct { WorkingDirectories []URI `json:"workingDirectories,omitempty"` } -// A shell command continuing outside its initiating tool call. +// A shell command continuing outside its initiating tool call. Covers shells +// tied to the agent's lifetime (attached) and shells that outlive it +// (detached). Whether a shell is attached is provider-specific and goes in its +// `_meta`. type BackgroundShellWork struct { // Identifier of this entry, unique within the owning chat across all kinds. // The host derives it however it likes (for example from the kind plus the @@ -1377,7 +1382,7 @@ type BackgroundShellWork struct { Label string `json:"label"` // ISO 8601 timestamp when the work started. StartedAt string `json:"startedAt"` - // Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + // Provider-specific metadata. Meta map[string]json.RawMessage `json:"_meta,omitempty"` Kind BackgroundWorkKind `json:"kind"` // Command line, displayed as plain text. @@ -1401,7 +1406,7 @@ type BackgroundSubagentWork struct { Label string `json:"label"` // ISO 8601 timestamp when the work started. StartedAt string `json:"startedAt"` - // Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + // Provider-specific metadata. Meta map[string]json.RawMessage `json:"_meta,omitempty"` Kind BackgroundWorkKind `json:"kind"` // The subagent's chat: the same chat the spawning tool call's @@ -5685,7 +5690,7 @@ func (u SessionInputRequest) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } -// BackgroundWork is work running outside the current turn that will resume the owning chat when it finishes. +// BackgroundWork is work that keeps running after the tool call that started it returns and will resume the owning chat when it finishes. type BackgroundWork struct { Value isBackgroundWork } 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 d2725f113..1c987d56f 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 @@ -1640,8 +1640,10 @@ data class ChatState( */ val activity: String? = null, /** - * Work running outside the current turn that will resume this chat when it - * finishes, such as background shells and subagents. Independent of turn state. + * Work that keeps running after the tool call that started it returns and + * will resume this chat when it finishes, such as background shells and + * subagents. Entries stay listed whether or not the turn that started them is + * still open. */ val backgroundWork: List? = null, /** @@ -1996,7 +1998,7 @@ data class BackgroundShellWork( */ val startedAt: String, /** - * Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + * Provider-specific metadata. */ @SerialName("_meta") val meta: Map? = null, @@ -2033,7 +2035,7 @@ data class BackgroundSubagentWork( */ val startedAt: String, /** - * Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + * Provider-specific metadata. */ @SerialName("_meta") val meta: Map? = null, diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 4a080645d..48e33b297 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1927,8 +1927,10 @@ pub struct ChatState { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, - /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells and subagents. Independent of turn state. + /// Work that keeps running after the tool call that started it returns and + /// will resume this chat when it finishes, such as background shells and + /// subagents. Entries stay listed whether or not the turn that started them is + /// still open. #[serde(default, skip_serializing_if = "Option::is_none")] pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) @@ -2049,7 +2051,10 @@ pub struct ChatSummary { pub working_directories: Option>, } -/// A shell command continuing outside its initiating tool call. +/// A shell command continuing outside its initiating tool call. Covers shells +/// tied to the agent's lifetime (attached) and shells that outlive it +/// (detached). Whether a shell is attached is provider-specific and goes in its +/// `_meta`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BackgroundShellWork { @@ -2063,7 +2068,7 @@ pub struct BackgroundShellWork { pub label: String, /// ISO 8601 timestamp when the work started. pub started_at: String, - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + /// Provider-specific metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// Command line, displayed as plain text. @@ -2090,7 +2095,7 @@ pub struct BackgroundSubagentWork { pub label: String, /// ISO 8601 timestamp when the work started. pub started_at: String, - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + /// Provider-specific metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, /// The subagent's chat: the same chat the spawning tool call's @@ -6283,7 +6288,7 @@ pub enum SessionInputRequest { #[serde(untagged)] Unknown(serde_json::Value), } -/// Work running outside the current turn that will resume the owning chat when it finishes. +/// Work that keeps running after the tool call that started it returns and will resume the owning chat when it finishes. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum BackgroundWork { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index bc911e3f1..8073a23dc 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1663,8 +1663,10 @@ public struct ChatState: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the chat is currently doing public var activity: String? - /// Work running outside the current turn that will resume this chat when it - /// finishes, such as background shells and subagents. Independent of turn state. + /// Work that keeps running after the tool call that started it returns and + /// will resume this chat when it finishes, such as background shells and + /// subagents. Entries stay listed whether or not the turn that started them is + /// still open. public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String @@ -2077,7 +2079,7 @@ public struct BackgroundShellWork: Codable, Sendable { public var label: String /// ISO 8601 timestamp when the work started. public var startedAt: String - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + /// Provider-specific metadata. public var meta: [String: AnyCodable]? public var kind: BackgroundWorkKind /// Command line, displayed as plain text. @@ -2128,7 +2130,7 @@ public struct BackgroundSubagentWork: Codable, Sendable { public var label: String /// ISO 8601 timestamp when the work started. public var startedAt: String - /// Provider-specific metadata, such as how a shell's lifetime is tied to its agent. + /// Provider-specific metadata. public var meta: [String: AnyCodable]? public var kind: BackgroundWorkKind /// The subagent's chat: the same chat the spawning tool call's diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 7df932061..5db44ecdf 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -141,8 +141,9 @@ For example, `(status & SessionStatus.InProgress) !== 0` is true for both `InPro Subscribable on a [Chat Channel](/specification/chat-channel) at `ahp-chat:/`. A session is a catalog of chats (`SessionState.chats`); each chat carries the per-conversation state — the turn history, the active turn and its streaming response parts (including live input requests), tool calls, steering/queued messages, and the user's in-progress draft. A session starts with a default chat (`SessionState.defaultChat`); hosts advertising the `multipleChats` capability let clients open more via `createChat`. -`backgroundWork` lists work running outside the current turn that will resume the -chat when it finishes, such as background shells and subagents. +`backgroundWork` lists work that keeps running after the tool call that started it +returns and will resume the chat when it finishes, such as background shells and +subagents. Hosts publish complete entries with `chat/backgroundWorkSet` and remove them with `chat/backgroundWorkRemoved` when they finish or are no longer tracked. Entry IDs are opaque, unique within a chat across all kinds, and scoped to that chat. Hosts mirror @@ -152,10 +153,12 @@ subscriber can discover its chats' background work without reading transcripts. Each entry has a `kind`, a `label`, and a start time. Every entry is unfinished work; hosts remove entries when the work finishes rather than marking them done. A shell entry adds its plain-text command and, when the host has one, the terminal carrying -its output. A subagent entry points to the subagent's own chat instead of repeating -its state. The kind set is non-exhaustive: clients should keep entries of unknown -kinds and may render them from the common fields. Provider-specific details, such as -how a shell's lifetime is tied to its agent, belong in `_meta`. +its output. Shells can be tied to the agent's lifetime (attached) or outlive it +(detached); that distinction is provider-specific and goes in the shell's `_meta`. A +subagent entry points to the subagent's own chat instead of repeating its state. The +kind set is non-exhaustive: clients should keep entries of unknown kinds and may +render them from the common fields. Other provider-specific details also belong in +`_meta`. The collection survives turn completion, cancellation, steering, and history truncation; those actions do not establish whether the work has stopped. Hosts must diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 44e7616a6..f82e81670 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -4974,7 +4974,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", @@ -5127,7 +5127,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." } }, "required": [ @@ -5138,7 +5138,7 @@ }, "BackgroundShellWork": { "type": "object", - "description": "A shell command continuing outside its initiating tool call.", + "description": "A shell command continuing outside its initiating tool call. Covers shells\ntied to the agent's lifetime (attached) and shells that outlive it\n(detached). Whether a shell is attached is provider-specific and goes in its\n`_meta`.", "properties": { "id": { "type": "string", @@ -5155,7 +5155,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "shell" @@ -5196,7 +5196,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "subagent" @@ -8562,7 +8562,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." }, "ChatOrigin": { "oneOf": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index aac4047b4..6b5422462 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4080,7 +4080,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", @@ -4233,7 +4233,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." } }, "required": [ @@ -4244,7 +4244,7 @@ }, "BackgroundShellWork": { "type": "object", - "description": "A shell command continuing outside its initiating tool call.", + "description": "A shell command continuing outside its initiating tool call. Covers shells\ntied to the agent's lifetime (attached) and shells that outlive it\n(detached). Whether a shell is attached is provider-specific and goes in its\n`_meta`.", "properties": { "id": { "type": "string", @@ -4261,7 +4261,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "shell" @@ -4302,7 +4302,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "subagent" @@ -10680,7 +10680,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." }, "ChatInputQuestion": { "oneOf": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 6d521a004..d59f64055 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2509,7 +2509,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", @@ -2662,7 +2662,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." } }, "required": [ @@ -2673,7 +2673,7 @@ }, "BackgroundShellWork": { "type": "object", - "description": "A shell command continuing outside its initiating tool call.", + "description": "A shell command continuing outside its initiating tool call. Covers shells\ntied to the agent's lifetime (attached) and shells that outlive it\n(detached). Whether a shell is attached is provider-specific and goes in its\n`_meta`.", "properties": { "id": { "type": "string", @@ -2690,7 +2690,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "shell" @@ -2731,7 +2731,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "subagent" @@ -7877,7 +7877,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." }, "ChatInputQuestion": { "oneOf": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 175b92832..4244ed422 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2687,7 +2687,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", @@ -2840,7 +2840,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." } }, "required": [ @@ -2851,7 +2851,7 @@ }, "BackgroundShellWork": { "type": "object", - "description": "A shell command continuing outside its initiating tool call.", + "description": "A shell command continuing outside its initiating tool call. Covers shells\ntied to the agent's lifetime (attached) and shells that outlive it\n(detached). Whether a shell is attached is provider-specific and goes in its\n`_meta`.", "properties": { "id": { "type": "string", @@ -2868,7 +2868,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "shell" @@ -2909,7 +2909,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "subagent" @@ -6403,7 +6403,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." }, "ChatInputQuestion": { "oneOf": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 05d41baed..f9ed28a69 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2420,7 +2420,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work running outside the current turn that will resume this chat when it\nfinishes, such as background shells and subagents. Independent of turn state." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", @@ -2573,7 +2573,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." } }, "required": [ @@ -2584,7 +2584,7 @@ }, "BackgroundShellWork": { "type": "object", - "description": "A shell command continuing outside its initiating tool call.", + "description": "A shell command continuing outside its initiating tool call. Covers shells\ntied to the agent's lifetime (attached) and shells that outlive it\n(detached). Whether a shell is attached is provider-specific and goes in its\n`_meta`.", "properties": { "id": { "type": "string", @@ -2601,7 +2601,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "shell" @@ -2642,7 +2642,7 @@ "_meta": { "type": "object", "additionalProperties": {}, - "description": "Provider-specific metadata, such as how a shell's lifetime is tied to its agent." + "description": "Provider-specific metadata." }, "kind": { "const": "subagent" @@ -6008,7 +6008,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work running outside the current turn that will resume the owning chat when\nit finishes. Clients that don't recognize a `kind` should keep the entry and\nmay render it from the common fields." + "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." }, "ChatOrigin": { "oneOf": [ diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index 55bcd135b..cc93e456c 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -1163,7 +1163,7 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { const BACKGROUND_WORK_UNION: UnionConfig = { name: 'BackgroundWork', discriminantField: 'kind', - doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', + doc: 'Work that keeps running after the tool call that started it returns and will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index d76ba5d35..88027de5e 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1135,7 +1135,7 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { const BACKGROUND_WORK_UNION: UnionConfig = { name: 'BackgroundWork', discriminantField: 'kind', - doc: 'BackgroundWork is work running outside the current turn that will resume the owning chat when it finishes.', + doc: 'BackgroundWork is work that keeps running after the tool call that started it returns and will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 1b22154f6..a91e06ae0 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1198,7 +1198,7 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { const BACKGROUND_WORK_UNION: UnionConfig = { name: 'BackgroundWork', discriminantField: 'kind', - doc: 'Work running outside the current turn that will resume the owning chat when it finishes.', + doc: 'Work that keeps running after the tool call that started it returns and will resume the owning chat when it finishes.', variants: [ { variantName: 'Shell', innerType: 'BackgroundShellWork', wireValue: 'shell' }, { variantName: 'Subagent', innerType: 'BackgroundSubagentWork', wireValue: 'subagent' }, diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 3e4557f5f..5539efa4a 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -55,8 +55,10 @@ export interface ChatState { /** Human-readable description of what the chat is currently doing */ activity?: string; /** - * Work running outside the current turn that will resume this chat when it - * finishes, such as background shells and subagents. Independent of turn state. + * Work that keeps running after the tool call that started it returns and + * will resume this chat when it finishes, such as background shells and + * subagents. Entries stay listed whether or not the turn that started them is + * still open. */ backgroundWork?: BackgroundWork[]; /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -218,12 +220,15 @@ interface BackgroundWorkBase { label: string; /** ISO 8601 timestamp when the work started. */ startedAt: string; - /** Provider-specific metadata, such as how a shell's lifetime is tied to its agent. */ + /** Provider-specific metadata. */ _meta?: Record; } /** - * A shell command continuing outside its initiating tool call. + * A shell command continuing outside its initiating tool call. Covers shells + * tied to the agent's lifetime (attached) and shells that outlive it + * (detached). Whether a shell is attached is provider-specific and goes in its + * `_meta`. * * @category Background Work */ @@ -255,9 +260,9 @@ export interface BackgroundSubagentWork extends BackgroundWorkBase { } /** - * Work running outside the current turn that will resume the owning chat when - * it finishes. Clients that don't recognize a `kind` should keep the entry and - * may render it from the common fields. + * Work that keeps running after the tool call that started it returns and will + * resume the owning chat when it finishes. Clients that don't recognize a + * `kind` should keep the entry and may render it from the common fields. * * @category Background Work */ From d513c6fbaf7075306651e49d4730464306d9662c Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Tue, 29 Sep 2026 22:28:43 -0700 Subject: [PATCH 11/14] Say that background work lists only active work Addresses review thread https://github.com/microsoft/agent-host-protocol/pull/482#discussion_r4139848501. Hosts remove an entry once its work ends rather than marking it completed. The state model guide already said so; say it on the field too. --- .../Generated/State.generated.cs | 3 ++- clients/go/ahptypes/state.generated.go | 3 ++- .../microsoft/agenthostprotocol/generated/State.generated.kt | 3 ++- clients/rust/crates/ahp-types/src/state.rs | 3 ++- .../Sources/AgentHostProtocol/Generated/State.generated.swift | 3 ++- schema/actions.schema.json | 2 +- schema/commands.schema.json | 2 +- schema/errors.schema.json | 2 +- schema/notifications.schema.json | 2 +- schema/state.schema.json | 2 +- types/channels-chat/state.ts | 3 ++- 11 files changed, 17 insertions(+), 11 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 08d455a25..5fecc7411 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1278,7 +1278,8 @@ public sealed class ChatState /// Work that keeps running after the tool call that started it returns and /// will resume this chat when it finishes, such as background shells and - /// subagents. Entries stay listed whether or not the turn that started them is + /// subagents. Only active work is listed: hosts remove an entry once the work + /// ends. Entries stay listed whether or not the turn that started them is /// still open. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? BackgroundWork { get; set; } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 79e760aa8..f07d7698d 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1265,7 +1265,8 @@ type ChatState struct { Activity *string `json:"activity,omitempty"` // Work that keeps running after the tool call that started it returns and // will resume this chat when it finishes, such as background shells and - // subagents. Entries stay listed whether or not the turn that started them is + // subagents. Only active work is listed: hosts remove an entry once the work + // ends. Entries stay listed whether or not the turn that started them is // still open. BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) 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 1c987d56f..bf38acc14 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 @@ -1642,7 +1642,8 @@ data class ChatState( /** * Work that keeps running after the tool call that started it returns and * will resume this chat when it finishes, such as background shells and - * subagents. Entries stay listed whether or not the turn that started them is + * subagents. Only active work is listed: hosts remove an entry once the work + * ends. Entries stay listed whether or not the turn that started them is * still open. */ val backgroundWork: List? = null, diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 48e33b297..16810e9d4 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1929,7 +1929,8 @@ pub struct ChatState { pub activity: Option, /// Work that keeps running after the tool call that started it returns and /// will resume this chat when it finishes, such as background shells and - /// subagents. Entries stay listed whether or not the turn that started them is + /// subagents. Only active work is listed: hosts remove an entry once the work + /// ends. Entries stay listed whether or not the turn that started them is /// still open. #[serde(default, skip_serializing_if = "Option::is_none")] pub background_work: Option>, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 8073a23dc..628bc7659 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1665,7 +1665,8 @@ public struct ChatState: Codable, Sendable { public var activity: String? /// Work that keeps running after the tool call that started it returns and /// will resume this chat when it finishes, such as background shells and - /// subagents. Entries stay listed whether or not the turn that started them is + /// subagents. Only active work is listed: hosts remove an entry once the work + /// ends. Entries stay listed whether or not the turn that started them is /// still open. public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) diff --git a/schema/actions.schema.json b/schema/actions.schema.json index f82e81670..5525381eb 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -4974,7 +4974,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 6b5422462..f0858c096 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4080,7 +4080,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index d59f64055..9769e6bb7 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2509,7 +2509,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 4244ed422..b36427e3e 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2687,7 +2687,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", diff --git a/schema/state.schema.json b/schema/state.schema.json index f9ed28a69..7396dbeba 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2420,7 +2420,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." }, "modifiedAt": { "type": "string", diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 5539efa4a..764f4450b 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -57,7 +57,8 @@ export interface ChatState { /** * Work that keeps running after the tool call that started it returns and * will resume this chat when it finishes, such as background shells and - * subagents. Entries stay listed whether or not the turn that started them is + * subagents. Only active work is listed: hosts remove an entry once the work + * ends. Entries stay listed whether or not the turn that started them is * still open. */ backgroundWork?: BackgroundWork[]; From ab694bfde0ae7f7b3fdb0e0877046288f3640469 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 30 Sep 2026 16:37:16 -0700 Subject: [PATCH 12/14] Shorten the background work description Addresses review thread https://github.com/microsoft/agent-host-protocol/pull/482#discussion_r4150108432. Background work may still be running when it notifies the chat, so drop "will resume this chat when it finishes" along with the tool call that started it. Say instead that an entry may have been started by an earlier turn rather than the activeTurn, and match the BackgroundWork doc and the state model guide. --- .../Generated/State.generated.cs | 7 +++---- clients/go/ahptypes/state.generated.go | 7 +++---- .../agenthostprotocol/generated/State.generated.kt | 7 +++---- clients/rust/crates/ahp-types/src/state.rs | 7 +++---- .../Generated/State.generated.swift | 7 +++---- docs/guide/state-model.md | 5 ++--- schema/actions.schema.json | 4 ++-- schema/commands.schema.json | 4 ++-- schema/errors.schema.json | 4 ++-- schema/notifications.schema.json | 4 ++-- schema/state.schema.json | 4 ++-- types/channels-chat/state.ts | 13 ++++++------- 12 files changed, 33 insertions(+), 40 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index d787218b4..3ff0983ae 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1284,11 +1284,10 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; set; } - /// Work that keeps running after the tool call that started it returns and - /// will resume this chat when it finishes, such as background shells and + /// Work running in the background for this chat, such as shells and /// subagents. Only active work is listed: hosts remove an entry once the work - /// ends. Entries stay listed whether or not the turn that started them is - /// still open. + /// ends. An entry may have been started by an earlier turn rather than the + /// {@link ChatState.activeTurn | activeTurn}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? BackgroundWork { get; set; } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 412c44bfd..40f2061b5 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1268,11 +1268,10 @@ type ChatState struct { Status SessionStatus `json:"status"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` - // Work that keeps running after the tool call that started it returns and - // will resume this chat when it finishes, such as background shells and + // Work running in the background for this chat, such as shells and // subagents. Only active work is listed: hosts remove an entry once the work - // ends. Entries stay listed whether or not the turn that started them is - // still open. + // ends. An entry may have been started by an earlier turn rather than the + // {@link ChatState.activeTurn | activeTurn}. BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` 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 78aaa4600..4abff60e6 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 @@ -1641,11 +1641,10 @@ data class ChatState( */ val activity: String? = null, /** - * Work that keeps running after the tool call that started it returns and - * will resume this chat when it finishes, such as background shells and + * Work running in the background for this chat, such as shells and * subagents. Only active work is listed: hosts remove an entry once the work - * ends. Entries stay listed whether or not the turn that started them is - * still open. + * ends. An entry may have been started by an earlier turn rather than the + * {@link ChatState.activeTurn | activeTurn}. */ val backgroundWork: List? = null, /** diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 6066450d1..2ff7f61af 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1928,11 +1928,10 @@ pub struct ChatState { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, - /// Work that keeps running after the tool call that started it returns and - /// will resume this chat when it finishes, such as background shells and + /// Work running in the background for this chat, such as shells and /// subagents. Only active work is listed: hosts remove an entry once the work - /// ends. Entries stay listed whether or not the turn that started them is - /// still open. + /// ends. An entry may have been started by an earlier turn rather than the + /// {@link ChatState.activeTurn | activeTurn}. #[serde(default, skip_serializing_if = "Option::is_none")] pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index b4f0bd827..25c3cc9e0 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1664,11 +1664,10 @@ public struct ChatState: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the chat is currently doing public var activity: String? - /// Work that keeps running after the tool call that started it returns and - /// will resume this chat when it finishes, such as background shells and + /// Work running in the background for this chat, such as shells and /// subagents. Only active work is listed: hosts remove an entry once the work - /// ends. Entries stay listed whether or not the turn that started them is - /// still open. + /// ends. An entry may have been started by an earlier turn rather than the + /// {@link ChatState.activeTurn | activeTurn}. public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 48a9fdb75..95b236625 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -141,9 +141,8 @@ For example, `(status & SessionStatus.InProgress) !== 0` is true for both `InPro Subscribable on a [Chat Channel](/specification/chat-channel) at `ahp-chat:/`. A session is a catalog of chats (`SessionState.chats`); each chat carries the per-conversation state — the turn history, the active turn and its streaming response parts (including live input requests), tool calls, steering/queued messages, and the user's in-progress draft. A session starts with a default chat (`SessionState.defaultChat`); hosts advertising the `multipleChats` capability let clients open more via `createChat`. -`backgroundWork` lists work that keeps running after the tool call that started it -returns and will resume the chat when it finishes, such as background shells and -subagents. +`backgroundWork` lists work running in the background for the chat, such as shells +and subagents. Hosts publish complete entries with `chat/backgroundWorkSet` and remove them with `chat/backgroundWorkRemoved` when they finish or are no longer tracked. Entry IDs are opaque, unique within a chat across all kinds, and scoped to that chat. Hosts mirror diff --git a/schema/actions.schema.json b/schema/actions.schema.json index b11bf6101..991a87e79 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -5022,7 +5022,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}." }, "modifiedAt": { "type": "string", @@ -8618,7 +8618,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." + "description": "Work running in the background for a chat, such as a shell or a subagent.\nClients that don't recognize a `kind` should keep the entry and may render it\nfrom the common fields." }, "ChatOrigin": { "oneOf": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 2db537c4e..41c05ea1a 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4241,7 +4241,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}." }, "modifiedAt": { "type": "string", @@ -10832,7 +10832,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." + "description": "Work running in the background for a chat, such as a shell or a subagent.\nClients that don't recognize a `kind` should keep the entry and may render it\nfrom the common fields." }, "ChatInputQuestion": { "oneOf": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index a5ebe377b..d9fef4052 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2513,7 +2513,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}." }, "modifiedAt": { "type": "string", @@ -7960,7 +7960,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." + "description": "Work running in the background for a chat, such as a shell or a subagent.\nClients that don't recognize a `kind` should keep the entry and may render it\nfrom the common fields." }, "ChatInputQuestion": { "oneOf": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 34e45bc57..60c47b275 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2691,7 +2691,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}." }, "modifiedAt": { "type": "string", @@ -6415,7 +6415,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." + "description": "Work running in the background for a chat, such as a shell or a subagent.\nClients that don't recognize a `kind` should keep the entry and may render it\nfrom the common fields." }, "ChatInputQuestion": { "oneOf": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 817daa646..a976c2cf0 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2424,7 +2424,7 @@ "items": { "$ref": "#/$defs/BackgroundWork" }, - "description": "Work that keeps running after the tool call that started it returns and\nwill resume this chat when it finishes, such as background shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. Entries stay listed whether or not the turn that started them is\nstill open." + "description": "Work running in the background for this chat, such as shells and\nsubagents. Only active work is listed: hosts remove an entry once the work\nends. An entry may have been started by an earlier turn rather than the\n{@link ChatState.activeTurn | activeTurn}." }, "modifiedAt": { "type": "string", @@ -6020,7 +6020,7 @@ "$ref": "#/$defs/BackgroundSubagentWork" } ], - "description": "Work that keeps running after the tool call that started it returns and will\nresume the owning chat when it finishes. Clients that don't recognize a\n`kind` should keep the entry and may render it from the common fields." + "description": "Work running in the background for a chat, such as a shell or a subagent.\nClients that don't recognize a `kind` should keep the entry and may render it\nfrom the common fields." }, "ChatOrigin": { "oneOf": [ diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index f7ec6cd58..988082e56 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -55,11 +55,10 @@ export interface ChatState { /** Human-readable description of what the chat is currently doing */ activity?: string; /** - * Work that keeps running after the tool call that started it returns and - * will resume this chat when it finishes, such as background shells and + * Work running in the background for this chat, such as shells and * subagents. Only active work is listed: hosts remove an entry once the work - * ends. Entries stay listed whether or not the turn that started them is - * still open. + * ends. An entry may have been started by an earlier turn rather than the + * {@link ChatState.activeTurn | activeTurn}. */ backgroundWork?: BackgroundWork[]; /** Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -277,9 +276,9 @@ export interface BackgroundSubagentWork extends BackgroundWorkBase { } /** - * Work that keeps running after the tool call that started it returns and will - * resume the owning chat when it finishes. Clients that don't recognize a - * `kind` should keep the entry and may render it from the common fields. + * Work running in the background for a chat, such as a shell or a subagent. + * Clients that don't recognize a `kind` should keep the entry and may render it + * from the common fields. * * @category Background Work */ From 82d07d55f3f915a377a067b3ebbc68e015e377d1 Mon Sep 17 00:00:00 2001 From: Anthony Kim Date: Wed, 30 Sep 2026 17:54:12 -0700 Subject: [PATCH 13/14] Keep background work off chat summaries Addresses review thread https://github.com/microsoft/agent-host-protocol/pull/482#discussion_r4150493760. Clients only show background work for a chat they have opened, so mirroring it into ChatSummary would broadcast every change to all session subscribers for no reader. Keep it on ChatState next to changesets, which is also absent from the summary, and drop the session/chatUpdated mirroring from the docs, the Go, Swift, and Rust reducers, and the reducer fixtures. --- .../Generated/Actions.generated.cs | 9 +-- .../Generated/State.generated.cs | 22 +++---- clients/go/ahp/reducers.go | 3 - clients/go/ahptypes/actions.generated.go | 3 +- clients/go/ahptypes/state.generated.go | 16 ++--- .../generated/Actions.generated.kt | 4 -- .../generated/State.generated.kt | 22 +++---- clients/rust/crates/ahp-types/src/actions.rs | 6 +- clients/rust/crates/ahp-types/src/state.rs | 19 +++--- clients/rust/crates/ahp/src/reducers.rs | 3 - .../Generated/Actions.generated.swift | 4 -- .../Generated/State.generated.swift | 24 ++++---- .../Sources/AgentHostProtocol/Reducers.swift | 1 - .../20260929-chat-background-work.json | 2 +- docs/guide/state-model.md | 6 +- schema/actions.schema.json | 32 +++------- schema/commands.schema.json | 32 +++------- schema/errors.schema.json | 32 +++------- schema/notifications.schema.json | 21 +++---- schema/state.schema.json | 21 +++---- scripts/generate-go.ts | 2 +- types/channels-chat/actions.ts | 3 +- types/channels-chat/state.ts | 20 ++++--- ...81-session-chatupdated-backgroundwork.json | 60 ------------------- ...ion-chatupdated-clears-backgroundwork.json | 53 ---------------- 25 files changed, 115 insertions(+), 305 deletions(-) delete mode 100644 types/test-cases/reducers/281-session-chatupdated-backgroundwork.json delete mode 100644 types/test-cases/reducers/282-session-chatupdated-clears-backgroundwork.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index 02574bb08..4a7486557 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -1729,7 +1729,7 @@ public sealed record ChatActivityChangedAction } /// Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn -/// state. Hosts mirror the resulting list through `session/chatUpdated`. +/// state. public sealed record ChatBackgroundWorkSetAction { public ActionType Type { get; init; } @@ -1738,8 +1738,7 @@ public sealed record ChatBackgroundWorkSetAction public required BackgroundWork Work { get; init; } } -/// Removes finished or no-longer-tracked background work; unknown IDs are a no-op. -/// Hosts mirror the resulting list through `session/chatUpdated`. +/// Removes finished or no-longer-tracked background work; unknown IDs are a no-op. public sealed record ChatBackgroundWorkRemovedAction { public ActionType Type { get; init; } @@ -2630,10 +2629,6 @@ public sealed record PartialChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; init; } - /// Background work, mirrored from {@link ChatState.backgroundWork}. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? BackgroundWork { get; init; } - /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ModifiedAt { get; init; } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 3ff0983ae..b416bdd5a 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1155,10 +1155,6 @@ public sealed class ChatSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; set; } - /// Background work, mirrored from {@link ChatState.backgroundWork}. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? BackgroundWork { get; set; } - /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public required string ModifiedAt { get; set; } @@ -1284,13 +1280,6 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Activity { get; set; } - /// Work running in the background for this chat, such as shells and - /// subagents. Only active work is listed: hosts remove an entry once the work - /// ends. An entry may have been started by an earlier turn rather than the - /// {@link ChatState.activeTurn | activeTurn}. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public List? BackgroundWork { get; set; } - /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public required string ModifiedAt { get; set; } @@ -1346,6 +1335,17 @@ public sealed class ChatState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Changesets { get; set; } + /// Work running in the background for this chat, such as shells and + /// subagents. Only active work is listed: hosts remove an entry once the work + /// ends. An entry may have been started by an earlier turn rather than the + /// {@link ChatState.activeTurn | activeTurn}. + /// + /// Like {@link ChatState.changesets | changesets}, this is intentionally + /// absent from {@link ChatSummary}; clients obtain it by subscribing to the + /// chat channel. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BackgroundWork { get; set; } + /// Completed turns public required List Turns { get; set; } diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 45aad0ad3..ebef72956 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -867,9 +867,6 @@ func mergeChatSummaryPartial(summary *ahptypes.ChatSummary, changes ahptypes.Par if changes.Activity != nil { summary.Activity = changes.Activity } - if changes.BackgroundWork != nil { - summary.BackgroundWork = changes.BackgroundWork - } if changes.ModifiedAt != nil { summary.ModifiedAt = *changes.ModifiedAt } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 27466d965..4a88b903b 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -662,7 +662,7 @@ type ChatActivityChangedAction struct { } // Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn -// state. Hosts mirror the resulting list through `session/chatUpdated`. +// state. type ChatBackgroundWorkSetAction struct { Type ActionType `json:"type"` // The complete entry. @@ -670,7 +670,6 @@ type ChatBackgroundWorkSetAction struct { } // Removes finished or no-longer-tracked background work; unknown IDs are a no-op. -// Hosts mirror the resulting list through `session/chatUpdated`. type ChatBackgroundWorkRemovedAction struct { Type ActionType `json:"type"` // The {@link BackgroundWorkBase.id | id} of the entry to remove. diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 40f2061b5..3ff43dff6 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1268,11 +1268,6 @@ type ChatState struct { Status SessionStatus `json:"status"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` - // Work running in the background for this chat, such as shells and - // subagents. Only active work is listed: hosts remove an entry once the work - // ends. An entry may have been started by an earlier turn rather than the - // {@link ChatState.activeTurn | activeTurn}. - BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` // Aggregate summary of file changes associated with this chat. Servers may @@ -1315,6 +1310,15 @@ type ChatState struct { // This catalogue is intentionally absent from {@link ChatSummary}; clients // obtain it by subscribing to the chat channel. Changesets []Changeset `json:"changesets,omitempty"` + // Work running in the background for this chat, such as shells and + // subagents. Only active work is listed: hosts remove an entry once the work + // ends. An entry may have been started by an earlier turn rather than the + // {@link ChatState.activeTurn | activeTurn}. + // + // Like {@link ChatState.changesets | changesets}, this is intentionally + // absent from {@link ChatSummary}; clients obtain it by subscribing to the + // chat channel. + BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Completed turns Turns []Turn `json:"turns"` // Cursor for loading older completed turns into this chat state. @@ -1358,8 +1362,6 @@ type ChatSummary struct { Status SessionStatus `json:"status"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` - // Background work, mirrored from {@link ChatState.backgroundWork}. - BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt string `json:"modifiedAt"` // Aggregate summary of file changes associated with this chat. Servers may 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 4fad486ee..3607b366b 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 @@ -1612,10 +1612,6 @@ data class PartialChatSummary( * Human-readable description of what the chat is currently doing */ val activity: String? = null, - /** - * Background work, mirrored from {@link ChatState.backgroundWork}. - */ - val backgroundWork: List? = null, /** * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ 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 4abff60e6..d85f16337 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 @@ -1640,13 +1640,6 @@ data class ChatState( * Human-readable description of what the chat is currently doing */ val activity: String? = null, - /** - * Work running in the background for this chat, such as shells and - * subagents. Only active work is listed: hosts remove an entry once the work - * ends. An entry may have been started by an earlier turn rather than the - * {@link ChatState.activeTurn | activeTurn}. - */ - val backgroundWork: List? = null, /** * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ @@ -1703,6 +1696,17 @@ data class ChatState( * obtain it by subscribing to the chat channel. */ val changesets: List? = null, + /** + * Work running in the background for this chat, such as shells and + * subagents. Only active work is listed: hosts remove an entry once the work + * ends. An entry may have been started by an earlier turn rather than the + * {@link ChatState.activeTurn | activeTurn}. + * + * Like {@link ChatState.changesets | changesets}, this is intentionally + * absent from {@link ChatSummary}; clients obtain it by subscribing to the + * chat channel. + */ + val backgroundWork: List? = null, /** * Completed turns */ @@ -1767,10 +1771,6 @@ data class ChatSummary( * Human-readable description of what the chat is currently doing */ val activity: String? = null, - /** - * Background work, mirrored from {@link ChatState.backgroundWork}. - */ - val backgroundWork: List? = null, /** * Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) */ diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index f2373f083..ebf56f342 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1086,7 +1086,7 @@ pub struct ChatActivityChangedAction { } /// Adds or replaces a {@link BackgroundWork} entry by `id`, independently of turn -/// state. Hosts mirror the resulting list through `session/chatUpdated`. +/// state. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatBackgroundWorkSetAction { @@ -1095,7 +1095,6 @@ pub struct ChatBackgroundWorkSetAction { } /// Removes finished or no-longer-tracked background work; unknown IDs are a no-op. -/// Hosts mirror the resulting list through `session/chatUpdated`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatBackgroundWorkRemovedAction { @@ -2290,9 +2289,6 @@ pub struct PartialChatSummary { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, - /// Background work, mirrored from {@link ChatState.backgroundWork}. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub background_work: 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, diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 2ff7f61af..427c9f86d 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1928,12 +1928,6 @@ pub struct ChatState { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, - /// Work running in the background for this chat, such as shells and - /// subagents. Only active work is listed: hosts remove an entry once the work - /// ends. An entry may have been started by an earlier turn rather than the - /// {@link ChatState.activeTurn | activeTurn}. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) pub modified_at: String, /// Aggregate summary of file changes associated with this chat. Servers may @@ -1982,6 +1976,16 @@ pub struct ChatState { /// obtain it by subscribing to the chat channel. #[serde(default, skip_serializing_if = "Option::is_none")] pub changesets: Option>, + /// Work running in the background for this chat, such as shells and + /// subagents. Only active work is listed: hosts remove an entry once the work + /// ends. An entry may have been started by an earlier turn rather than the + /// {@link ChatState.activeTurn | activeTurn}. + /// + /// Like {@link ChatState.changesets | changesets}, this is intentionally + /// absent from {@link ChatSummary}; clients obtain it by subscribing to the + /// chat channel. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub background_work: Option>, /// Completed turns pub turns: Vec, /// Cursor for loading older completed turns into this chat state. @@ -2034,9 +2038,6 @@ pub struct ChatSummary { /// Human-readable description of what the chat is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, - /// Background work, mirrored from {@link ChatState.backgroundWork}. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub background_work: Option>, /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) pub modified_at: String, /// Aggregate summary of file changes associated with this chat. Servers may diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 427e4b31f..47b30a15e 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -746,9 +746,6 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - if let Some(activity) = &a.changes.activity { chat.activity = Some(activity.clone()); } - if let Some(work) = &a.changes.background_work { - chat.background_work = Some(work.clone()); - } if let Some(modified_at) = &a.changes.modified_at { chat.modified_at = modified_at.clone(); } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index b767eeb46..991813b78 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -2474,8 +2474,6 @@ public struct PartialChatSummary: Codable, Sendable { public var status: SessionStatus? /// Human-readable description of what the chat is currently doing public var activity: String? - /// Background work, mirrored from {@link ChatState.backgroundWork}. - public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String? /// Aggregate summary of file changes associated with this chat. Servers may @@ -2504,7 +2502,6 @@ public struct PartialChatSummary: Codable, Sendable { title: String? = nil, status: SessionStatus? = nil, activity: String? = nil, - backgroundWork: [BackgroundWork]? = nil, modifiedAt: String? = nil, changes: ChangesSummary? = nil, origin: ChatOrigin? = nil, @@ -2516,7 +2513,6 @@ public struct PartialChatSummary: Codable, Sendable { self.title = title self.status = status self.activity = activity - self.backgroundWork = backgroundWork self.modifiedAt = modifiedAt self.changes = changes self.origin = origin diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 25c3cc9e0..8abefa6b4 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1664,11 +1664,6 @@ public struct ChatState: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the chat is currently doing public var activity: String? - /// Work running in the background for this chat, such as shells and - /// subagents. Only active work is listed: hosts remove an entry once the work - /// ends. An entry may have been started by an earlier turn rather than the - /// {@link ChatState.activeTurn | activeTurn}. - public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String /// Aggregate summary of file changes associated with this chat. Servers may @@ -1711,6 +1706,15 @@ public struct ChatState: Codable, Sendable { /// This catalogue is intentionally absent from {@link ChatSummary}; clients /// obtain it by subscribing to the chat channel. public var changesets: [Changeset]? + /// Work running in the background for this chat, such as shells and + /// subagents. Only active work is listed: hosts remove an entry once the work + /// ends. An entry may have been started by an earlier turn rather than the + /// {@link ChatState.activeTurn | activeTurn}. + /// + /// Like {@link ChatState.changesets | changesets}, this is intentionally + /// absent from {@link ChatSummary}; clients obtain it by subscribing to the + /// chat channel. + public var backgroundWork: [BackgroundWork]? /// Completed turns public var turns: [Turn] /// Cursor for loading older completed turns into this chat state. @@ -1746,7 +1750,6 @@ public struct ChatState: Codable, Sendable { case title case status case activity - case backgroundWork case modifiedAt case changes case origin @@ -1754,6 +1757,7 @@ public struct ChatState: Codable, Sendable { case interactivity case workingDirectories case changesets + case backgroundWork case turns case turnsNextCursor case activeTurn @@ -1768,7 +1772,6 @@ public struct ChatState: Codable, Sendable { title: String, status: SessionStatus, activity: String? = nil, - backgroundWork: [BackgroundWork]? = nil, modifiedAt: String, changes: ChangesSummary? = nil, origin: ChatOrigin? = nil, @@ -1776,6 +1779,7 @@ public struct ChatState: Codable, Sendable { interactivity: ChatInteractivity? = nil, workingDirectories: [String]? = nil, changesets: [Changeset]? = nil, + backgroundWork: [BackgroundWork]? = nil, turns: [Turn], turnsNextCursor: String? = nil, activeTurn: ActiveTurn? = nil, @@ -1788,7 +1792,6 @@ public struct ChatState: Codable, Sendable { self.title = title self.status = status self.activity = activity - self.backgroundWork = backgroundWork self.modifiedAt = modifiedAt self.changes = changes self.origin = origin @@ -1796,6 +1799,7 @@ public struct ChatState: Codable, Sendable { self.interactivity = interactivity self.workingDirectories = workingDirectories self.changesets = changesets + self.backgroundWork = backgroundWork self.turns = turns self.turnsNextCursor = turnsNextCursor self.activeTurn = activeTurn @@ -1815,8 +1819,6 @@ public struct ChatSummary: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the chat is currently doing public var activity: String? - /// Background work, mirrored from {@link ChatState.backgroundWork}. - public var backgroundWork: [BackgroundWork]? /// Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) public var modifiedAt: String /// Aggregate summary of file changes associated with this chat. Servers may @@ -1845,7 +1847,6 @@ public struct ChatSummary: Codable, Sendable { title: String, status: SessionStatus, activity: String? = nil, - backgroundWork: [BackgroundWork]? = nil, modifiedAt: String, changes: ChangesSummary? = nil, origin: ChatOrigin? = nil, @@ -1857,7 +1858,6 @@ public struct ChatSummary: Codable, Sendable { self.title = title self.status = status self.activity = activity - self.backgroundWork = backgroundWork self.modifiedAt = modifiedAt self.changes = changes self.origin = origin diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index bb86d759a..6f4ef620c 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -1100,7 +1100,6 @@ private func mergeChatSummaryChanges(_ summary: inout ChatSummary, changes: Part 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 work = changes.backgroundWork { summary.backgroundWork = work } if let modifiedAt = changes.modifiedAt { summary.modifiedAt = modifiedAt } if let changesSummary = changes.changes { summary.changes = changesSummary } if let origin = changes.origin { summary.origin = origin } diff --git a/docs/.changes/20260929-chat-background-work.json b/docs/.changes/20260929-chat-background-work.json index 3a534a88f..479c8be16 100644 --- a/docs/.changes/20260929-chat-background-work.json +++ b/docs/.changes/20260929-chat-background-work.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Expose chat-owned background work (background shells and subagents) in chat state and session chat summaries, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime." + "message": "Expose chat-owned background work (background shells and subagents) in chat state, with `chat/backgroundWorkSet` and `chat/backgroundWorkRemoved` actions independent of turn lifetime." } diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index 95b236625..03d6874e3 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -145,9 +145,9 @@ Subscribable on a [Chat Channel](/specification/chat-channel) at `ahp-chat:/ Date: Wed, 30 Sep 2026 18:01:55 -0700 Subject: [PATCH 14/14] Drop leftover summary background work from clients 82d07d55 removed ChatSummary.backgroundWork but left references to it in the hand-written .NET and Kotlin chatUpdated reducers, a Rust test literal, and Go's hand-written PartialChatSummary, which broke the dotnet, kotlin, and rust CI jobs. --- clients/dotnet/src/AgentHostProtocol/Reducers.cs | 1 - clients/go/ahptypes/common.go | 2 -- .../src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt | 1 - clients/rust/crates/ahp/src/reducers.rs | 1 - 4 files changed, 5 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs index 0c13bd0be..af977f375 100644 --- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs +++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs @@ -1873,7 +1873,6 @@ private static ReduceOutcome ApplySessionChatUpdated(SessionState state, Session if (ch.Title is not null) { s.Title = ch.Title; } if (ch.Status is not null) { s.Status = ch.Status.Value; } if (ch.Activity is not null) { s.Activity = ch.Activity; } - if (ch.BackgroundWork is not null) { s.BackgroundWork = ch.BackgroundWork; } if (ch.ModifiedAt is not null) { s.ModifiedAt = ch.ModifiedAt; } if (ch.Changes is not null) { s.Changes = ch.Changes; } if (ch.Origin is not null) { s.Origin = ch.Origin; } diff --git a/clients/go/ahptypes/common.go b/clients/go/ahptypes/common.go index 9fd0feabd..597d73725 100644 --- a/clients/go/ahptypes/common.go +++ b/clients/go/ahptypes/common.go @@ -79,8 +79,6 @@ type PartialChatSummary struct { Status *SessionStatus `json:"status,omitempty"` // Human-readable description of what the chat is currently doing Activity *string `json:"activity,omitempty"` - // Preserve an explicitly empty background work list in a summary update. - BackgroundWork *[]BackgroundWork `json:"backgroundWork,omitempty"` // Last modification timestamp (ISO 8601, e.g. `"2025-03-10T18:42:03.123Z"`) ModifiedAt *string `json:"modifiedAt,omitempty"` // Aggregate summary of file changes associated with this chat 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 ad1ce48a0..8fd22a745 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -595,7 +595,6 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat title = c.title ?: prior.title, status = c.status ?: prior.status, activity = c.activity ?: prior.activity, - backgroundWork = c.backgroundWork ?: prior.backgroundWork, modifiedAt = c.modifiedAt ?: prior.modifiedAt, changes = c.changes ?: prior.changes, origin = c.origin ?: prior.origin, diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 47b30a15e..76602c966 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -2448,7 +2448,6 @@ mod tests { title: "c1".into(), status: SessionStatus::Idle.bits(), activity: None, - background_work: None, modified_at: "1970-01-01T00:00:00.000Z".into(), changes: None, origin: None,