From a8ff2e45e5828589fa1bd2c362f893b3ec27a0be Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Wed, 5 Aug 2026 16:51:52 +0200 Subject: [PATCH 1/8] agent-host: feat: add shared automations protocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 935e37c8-2b68-40b1-9273-f195cb3af9a0 --- clients/go/ahp/client.go | 42 + clients/go/ahp/client_test.go | 114 + clients/go/ahp/hosts/hosts.go | 72 +- clients/go/ahp/hosts/hosts_test.go | 73 +- clients/go/ahp/multi_host_state_mirror.go | 68 +- .../go/ahp/multi_host_state_mirror_test.go | 52 + clients/go/ahp/reducers.go | 97 + clients/go/ahp/reducers_fixture_test.go | 4 + clients/go/ahptypes/actions.generated.go | 487 +- clients/go/ahptypes/ahptypes_test.go | 21 + clients/go/ahptypes/commands.generated.go | 127 + .../go/ahptypes/notifications.generated.go | 17 + clients/go/ahptypes/roundtrip_fixture_test.go | 4 + clients/go/ahptypes/state.generated.go | 694 +- .../microsoft/agenthostprotocol/Reducers.kt | 129 +- .../generated/Actions.generated.kt | 128 +- .../generated/Commands.generated.kt | 173 +- .../generated/Errors.generated.kt | 1 + .../generated/Messages.generated.kt | 1 + .../generated/Notifications.generated.kt | 23 + .../generated/State.generated.kt | 677 +- .../DiscriminatedUnionTest.kt | 19 + .../FixtureDrivenReducerTest.kt | 25 + .../agenthostprotocol/RoundTripCorpusTest.kt | 2 + clients/rust/crates/ahp-types/src/actions.rs | 127 +- clients/rust/crates/ahp-types/src/commands.rs | 197 +- .../crates/ahp-types/src/notifications.rs | 29 +- clients/rust/crates/ahp-types/src/state.rs | 475 +- .../ahp-types/tests/roundtrip_corpus.rs | 3 +- clients/rust/crates/ahp/src/client.rs | 44 +- clients/rust/crates/ahp/src/hosts/runtime.rs | 10 + clients/rust/crates/ahp/src/hosts/types.rs | 12 +- clients/rust/crates/ahp/src/lib.rs | 3 +- .../crates/ahp/src/multi_host_state_mirror.rs | 60 +- clients/rust/crates/ahp/src/reducers.rs | 148 +- .../rust/crates/ahp/tests/client_roundtrip.rs | 93 + clients/rust/crates/ahp/tests/hosts.rs | 60 +- .../ahp/tests/multi_host_state_mirror.rs | 114 + .../Generated/Actions.generated.swift | 210 + .../Generated/Commands.generated.swift | 322 +- .../Generated/Notifications.generated.swift | 44 + .../Generated/State.generated.swift | 1178 ++- .../Sources/AgentHostProtocol/Reducers.swift | 71 +- .../AgentHostProtocolClient/AHPClient.swift | 21 + .../AHPClientEvents.swift | 3 + .../AHPStateMirror.swift | 26 +- .../Hosts/HostHandle.swift | 8 +- .../Hosts/HostRuntime.swift | 6 +- .../Hosts/HostShared.swift | 2 + .../Hosts/MultiHostClient.swift | 6 +- .../MultiHostStateMirror.swift | 35 +- .../AHPClientTests.swift | 97 + .../MultiHostClientTests.swift | 78 +- .../MultiHostStateMirrorTests.swift | 89 + .../TypesRoundTripFixtureTests.swift | 2 + .../AutomationUnionTests.swift | 16 + .../FixtureDrivenReducerTests.swift | 8 + .../ReducersTests.swift | 7 + clients/typescript/src/client/client.ts | 18 + clients/typescript/src/client/events.ts | 6 + .../typescript/src/client/hosts/runtime.ts | 12 + .../src/client/hosts/state-mirror.ts | 46 + clients/typescript/src/client/hosts/types.ts | 3 + clients/typescript/src/client/state-mirror.ts | 36 + clients/typescript/test/client.test.ts | 29 + .../typescript/test/types-round-trip.test.ts | 3 +- .../.changes/20260805-shared-automations.json | 4 + docs/guide/automations.md | 46 + docs/specification/automation-channel.md | 68 + docs/specification/automation-run-channel.md | 53 + docs/specification/overview.md | 2 + docs/specification/root-channel.md | 6 + schema/actions.schema.json | 1146 ++- schema/commands.schema.json | 7846 ++++++++++------- schema/errors.schema.json | 1786 +++- schema/notifications.schema.json | 1147 ++- schema/state.schema.json | 1073 ++- scripts/find-protocol-sources.ts | 2 + scripts/generate-action-origin.ts | 64 +- scripts/generate-go.ts | 178 +- scripts/generate-json-schema.test.ts | 14 + scripts/generate-kotlin.ts | 150 +- scripts/generate-markdown.ts | 36 + scripts/generate-rust.ts | 141 +- scripts/generate-swift.ts | 135 +- types/action-origin.generated.ts | 69 + types/actions.ts | 2 + types/channels-automation-run/actions.ts | 54 + types/channels-automation-run/reducer.ts | 75 + types/channels-automation-run/state.ts | 150 + types/channels-automation/actions.ts | 39 + types/channels-automation/commands.ts | 142 + types/channels-automation/reducer.ts | 73 + types/channels-automation/state.ts | 199 + types/channels-root/notifications.ts | 40 + types/channels-session/state.ts | 17 + types/commands.ts | 1 + types/common/actions.ts | 39 +- types/common/commands.ts | 39 + types/common/messages.ts | 29 + types/common/state.ts | 4 +- types/index.ts | 2 + types/messages.test.ts | 2 + types/reducers.test.ts | 14 +- types/reducers.ts | 2 + types/state.ts | 2 + ...ion-definitionchanged-clears-next-run.json | 47 + .../264-automation-run-summary-lifecycle.json | 105 + .../265-automation-run-session-lifecycle.json | 41 + ...266-automation-run-artifact-lifecycle.json | 44 + ...tomation-runsloaded-deduplicates-page.json | 62 + ...-terminal-lifecycle-clears-operations.json | 43 + .../round-trips/041-automation-snapshot.json | 96 + .../042-automation-run-snapshot.json | 54 + .../043-automation-capabilities.json | 38 + types/version/message-checks.ts | 13 +- types/version/registry.ts | 14 + 117 files changed, 18091 insertions(+), 4164 deletions(-) create mode 100644 clients/go/ahp/multi_host_state_mirror_test.go create mode 100644 clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift create mode 100644 docs/.changes/20260805-shared-automations.json create mode 100644 docs/guide/automations.md create mode 100644 docs/specification/automation-channel.md create mode 100644 docs/specification/automation-run-channel.md create mode 100644 types/channels-automation-run/actions.ts create mode 100644 types/channels-automation-run/reducer.ts create mode 100644 types/channels-automation-run/state.ts create mode 100644 types/channels-automation/actions.ts create mode 100644 types/channels-automation/commands.ts create mode 100644 types/channels-automation/reducer.ts create mode 100644 types/channels-automation/state.ts create mode 100644 types/test-cases/reducers/263-automation-definitionchanged-clears-next-run.json create mode 100644 types/test-cases/reducers/264-automation-run-summary-lifecycle.json create mode 100644 types/test-cases/reducers/265-automation-run-session-lifecycle.json create mode 100644 types/test-cases/reducers/266-automation-run-artifact-lifecycle.json create mode 100644 types/test-cases/reducers/267-automation-runsloaded-deduplicates-page.json create mode 100644 types/test-cases/reducers/268-automation-run-terminal-lifecycle-clears-operations.json create mode 100644 types/test-cases/round-trips/041-automation-snapshot.json create mode 100644 types/test-cases/round-trips/042-automation-run-snapshot.json create mode 100644 types/test-cases/round-trips/043-automation-capabilities.json diff --git a/clients/go/ahp/client.go b/clients/go/ahp/client.go index 1938bf369..7e201b43b 100644 --- a/clients/go/ahp/client.go +++ b/clients/go/ahp/client.go @@ -73,6 +73,30 @@ type SubscriptionEventSessionSummaryChanged struct { func (SubscriptionEventSessionSummaryChanged) isSubscriptionEvent() {} +// SubscriptionEventAutomationAdded mirrors the `root/automationAdded` +// notification. +type SubscriptionEventAutomationAdded struct { + Params ahptypes.AutomationAddedParams +} + +func (SubscriptionEventAutomationAdded) isSubscriptionEvent() {} + +// SubscriptionEventAutomationRemoved mirrors the +// `root/automationRemoved` notification. +type SubscriptionEventAutomationRemoved struct { + Params ahptypes.AutomationRemovedParams +} + +func (SubscriptionEventAutomationRemoved) isSubscriptionEvent() {} + +// SubscriptionEventAutomationSummaryChanged mirrors the +// `root/automationSummaryChanged` notification. +type SubscriptionEventAutomationSummaryChanged struct { + Params ahptypes.AutomationSummaryChangedParams +} + +func (SubscriptionEventAutomationSummaryChanged) isSubscriptionEvent() {} + // SubscriptionEventAuthRequired mirrors the `auth/required` // notification. type SubscriptionEventAuthRequired struct { @@ -499,6 +523,24 @@ func (c *Client) handleNotification(n ahptypes.JsonRpcNotification) { return } c.fanOut(p.Channel, SubscriptionEventSessionSummaryChanged{Params: p}) + case "root/automationAdded": + var p ahptypes.AutomationAddedParams + if err := json.Unmarshal(n.Params, &p); err != nil { + return + } + c.fanOut(p.Channel, SubscriptionEventAutomationAdded{Params: p}) + case "root/automationRemoved": + var p ahptypes.AutomationRemovedParams + if err := json.Unmarshal(n.Params, &p); err != nil { + return + } + c.fanOut(p.Channel, SubscriptionEventAutomationRemoved{Params: p}) + case "root/automationSummaryChanged": + var p ahptypes.AutomationSummaryChangedParams + if err := json.Unmarshal(n.Params, &p); err != nil { + return + } + c.fanOut(p.Channel, SubscriptionEventAutomationSummaryChanged{Params: p}) case "auth/required": var p ahptypes.AuthRequiredParams if err := json.Unmarshal(n.Params, &p); err != nil { diff --git a/clients/go/ahp/client_test.go b/clients/go/ahp/client_test.go index 43340850b..0a85043b1 100644 --- a/clients/go/ahp/client_test.go +++ b/clients/go/ahp/client_test.go @@ -607,6 +607,120 @@ func TestClientSubscriptionFanOut(t *testing.T) { } } +func TestClientAutomationCatalogueNotifications(t *testing.T) { + clientSide, serverSide := newMemTransportPair() + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + client, err := Connect(ctx, clientSide, DefaultConfig()) + if err != nil { + t.Fatalf("Connect: %v", err) + } + defer client.Shutdown(context.Background()) + + sub := client.AttachSubscription(ahptypes.RootResourceURI) + stream := client.Events() + automationURI := ahptypes.URI("ahp-automation:/nightly") + + tests := []struct { + name string + method string + params any + check func(t *testing.T, event SubscriptionEvent) + }{ + { + name: "added", + method: "root/automationAdded", + params: ahptypes.AutomationAddedParams{ + Channel: ahptypes.RootResourceURI, + Summary: ahptypes.AutomationSummary{Resource: automationURI}, + }, + check: func(t *testing.T, event SubscriptionEvent) { + t.Helper() + added, ok := event.(SubscriptionEventAutomationAdded) + if !ok { + t.Fatalf("got %T, want SubscriptionEventAutomationAdded", event) + } + if added.Params.Summary.Resource != automationURI { + t.Errorf("resource = %q, want %q", added.Params.Summary.Resource, automationURI) + } + }, + }, + { + name: "removed", + method: "root/automationRemoved", + params: ahptypes.AutomationRemovedParams{ + Channel: ahptypes.RootResourceURI, + Automation: automationURI, + }, + check: func(t *testing.T, event SubscriptionEvent) { + t.Helper() + removed, ok := event.(SubscriptionEventAutomationRemoved) + if !ok { + t.Fatalf("got %T, want SubscriptionEventAutomationRemoved", event) + } + if removed.Params.Automation != automationURI { + t.Errorf("automation = %q, want %q", removed.Params.Automation, automationURI) + } + }, + }, + { + name: "summary changed", + method: "root/automationSummaryChanged", + params: ahptypes.AutomationSummaryChangedParams{ + Channel: ahptypes.RootResourceURI, + Summary: ahptypes.AutomationSummary{Resource: automationURI}, + }, + check: func(t *testing.T, event SubscriptionEvent) { + t.Helper() + changed, ok := event.(SubscriptionEventAutomationSummaryChanged) + if !ok { + t.Fatalf("got %T, want SubscriptionEventAutomationSummaryChanged", event) + } + if changed.Params.Summary.Resource != automationURI { + t.Errorf("resource = %q, want %q", changed.Params.Summary.Resource, automationURI) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params, err := json.Marshal(tt.params) + if err != nil { + t.Fatalf("marshal params: %v", err) + } + wire, err := EncodeMessage(ahptypes.JsonRpcMessage{Notification: &ahptypes.JsonRpcNotification{ + JsonRpc: ahptypes.JsonRpcV2, + Method: tt.method, + Params: params, + }}) + if err != nil { + t.Fatalf("encode notification: %v", err) + } + if err := serverSide.Send(ctx, wire); err != nil { + t.Fatalf("send notification: %v", err) + } + + select { + case event := <-sub.Events(): + tt.check(t, event) + case <-ctx.Done(): + t.Fatal("subscription did not receive event") + } + + select { + case event := <-stream.Events(): + if event.Channel != ahptypes.RootResourceURI { + t.Errorf("channel = %q, want %q", event.Channel, ahptypes.RootResourceURI) + } + tt.check(t, event.Event) + case <-ctx.Done(): + t.Fatal("top-level stream did not receive event") + } + }) + } +} + // TestClientShutdownFailsInFlightRequest confirms a Shutdown unblocks // any pending request with ErrShutdown. func TestClientShutdownFailsInFlightRequest(t *testing.T) { diff --git a/clients/go/ahp/hosts/hosts.go b/clients/go/ahp/hosts/hosts.go index fc42a06e5..2e50e6f3e 100644 --- a/clients/go/ahp/hosts/hosts.go +++ b/clients/go/ahp/hosts/hosts.go @@ -213,6 +213,7 @@ type HostHandle struct { ClientID string State HostState ProtocolVersion string + Automations *ahptypes.AutomationCapabilities Agents []ahptypes.AgentInfo Sessions []ahptypes.SessionSummary Terminals []ahptypes.TerminalInfo @@ -400,21 +401,22 @@ var ErrDuplicateHost = errors.New("hosts: host id already registered") // hostState is the per-host bookkeeping the multi-host runtime owns. type hostState struct { - id HostID - label string - cfg HostConfig - mu sync.RWMutex - client *ahp.Client - state HostState - clientID string - protoVer string - agents []ahptypes.AgentInfo - sessions []ahptypes.SessionSummary - terminals []ahptypes.TerminalInfo - updatedAt time.Time - generation uint64 - cancel context.CancelFunc - supervised sync.WaitGroup + id HostID + label string + cfg HostConfig + mu sync.RWMutex + client *ahp.Client + state HostState + clientID string + protoVer string + automations *ahptypes.AutomationCapabilities + agents []ahptypes.AgentInfo + sessions []ahptypes.SessionSummary + terminals []ahptypes.TerminalInfo + updatedAt time.Time + generation uint64 + cancel context.CancelFunc + supervised sync.WaitGroup } // MultiHostClient is the public multi-host registry + reconnect @@ -573,6 +575,7 @@ func (m *MultiHostClient) openHost(ctx context.Context, hs *hostState) error { hs.mu.Lock() hs.client = client hs.protoVer = result.ProtocolVersion + hs.automations = cloneAutomationCapabilities(result.Automations) hs.generation++ hs.mu.Unlock() @@ -716,6 +719,7 @@ func (m *MultiHostClient) snapshotHandle(hs *hostState) *HostHandle { ClientID: hs.clientID, State: hs.state, ProtocolVersion: hs.protoVer, + Automations: cloneAutomationCapabilities(hs.automations), Agents: append([]ahptypes.AgentInfo(nil), hs.agents...), Sessions: append([]ahptypes.SessionSummary(nil), hs.sessions...), Terminals: append([]ahptypes.TerminalInfo(nil), hs.terminals...), @@ -723,6 +727,44 @@ func (m *MultiHostClient) snapshotHandle(hs *hostState) *HostHandle { } } +func cloneAutomationCapabilities(capabilities *ahptypes.AutomationCapabilities) *ahptypes.AutomationCapabilities { + if capabilities == nil { + return nil + } + + clone := *capabilities + if capabilities.Create != nil { + value := *capabilities.Create + clone.Create = &value + } + if capabilities.Schedules != nil { + value := *capabilities.Schedules + value.Kinds = append([]ahptypes.AutomationScheduleKind(nil), capabilities.Schedules.Kinds...) + if capabilities.Schedules.Cron != nil { + cron := *capabilities.Schedules.Cron + if capabilities.Schedules.Cron.MinIntervalMinutes != nil { + minIntervalMinutes := *capabilities.Schedules.Cron.MinIntervalMinutes + cron.MinIntervalMinutes = &minIntervalMinutes + } + value.Cron = &cron + } + clone.Schedules = &value + } + if capabilities.RunCancellation != nil { + value := *capabilities.RunCancellation + clone.RunCancellation = &value + } + if capabilities.SchedulePreview != nil { + value := *capabilities.SchedulePreview + clone.SchedulePreview = &value + } + if capabilities.RunHistoryLimit != nil { + value := *capabilities.RunHistoryLimit + clone.RunHistoryLimit = &value + } + return &clone +} + // ClientHandle returns a generation-checked [HostClientHandle] for // the named host, or ErrUnknownHost if the host is not registered. func (m *MultiHostClient) ClientHandle(id HostID) (*HostClientHandle, error) { diff --git a/clients/go/ahp/hosts/hosts_test.go b/clients/go/ahp/hosts/hosts_test.go index 258c4bc05..ebb0f645d 100644 --- a/clients/go/ahp/hosts/hosts_test.go +++ b/clients/go/ahp/hosts/hosts_test.go @@ -66,6 +66,12 @@ func (t *fakeTransport) Close(_ context.Context) error { // runFakeServer responds to one Initialize request with a stub // InitializeResult. It exits when the transport closes. func runFakeServer(t *testing.T, serverSide *fakeTransport) { + runFakeServerWithInitializeResult(t, serverSide, ahptypes.InitializeResult{ + ProtocolVersion: ahptypes.ProtocolVersion, + }) +} + +func runFakeServerWithInitializeResult(t *testing.T, serverSide *fakeTransport, initializeResult ahptypes.InitializeResult) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() @@ -82,7 +88,7 @@ func runFakeServer(t *testing.T, serverSide *fakeTransport) { continue } if parsed.Request.Method == "initialize" { - result, _ := json.Marshal(ahptypes.InitializeResult{ProtocolVersion: ahptypes.ProtocolVersion}) + result, _ := json.Marshal(initializeResult) resp := ahptypes.JsonRpcMessage{SuccessResponse: &ahptypes.JsonRpcSuccessResponse{ JsonRpc: ahptypes.JsonRpcV2, ID: parsed.Request.ID, @@ -94,6 +100,71 @@ func runFakeServer(t *testing.T, serverSide *fakeTransport) { } } +func TestAutomationCapabilitiesUpdatedAcrossReconnect(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + multi := NewMultiHostClient() + defer multi.Shutdown(context.Background()) + + servers := make(chan *fakeTransport, 2) + attempt := 0 + cfg := NewHostConfig("automation-host", "Automation Host", func(_ context.Context, _ HostID) (ahp.Transport, error) { + attempt++ + clientSide, serverSide := newFakePair() + lifetime := ahptypes.AutomationExecutionLifetimeHostLifetime + if attempt > 1 { + lifetime = ahptypes.AutomationExecutionLifetimeManaged + } + go runFakeServerWithInitializeResult(t, serverSide, ahptypes.InitializeResult{ + ProtocolVersion: ahptypes.ProtocolVersion, + Automations: &ahptypes.AutomationCapabilities{ + Execution: ahptypes.AutomationExecutionCapabilities{Lifetime: lifetime}, + }, + }) + servers <- serverSide + return clientSide, nil + }) + cfg.ReconnectPolicy = ReconnectPolicy{ + MaxAttempts: 2, + InitialBackoff: time.Millisecond, + MaxBackoff: time.Millisecond, + BackoffMultiplier: 1, + ResetOnSuccess: true, + } + + handle, err := multi.AddHost(ctx, cfg) + if err != nil { + t.Fatalf("AddHost: %v", err) + } + if handle.Automations == nil { + t.Fatal("initial Automations is nil") + } + if got := handle.Automations.Execution.Lifetime; got != ahptypes.AutomationExecutionLifetimeHostLifetime { + t.Fatalf("initial lifetime = %q, want %q", got, ahptypes.AutomationExecutionLifetimeHostLifetime) + } + + firstServer := <-servers + if err := firstServer.Close(ctx); err != nil { + t.Fatalf("close first server: %v", err) + } + + for { + handle = multi.Host(cfg.ID) + if handle != nil && + handle.State.Kind == HostStateConnected && + handle.Automations != nil && + handle.Automations.Execution.Lifetime == ahptypes.AutomationExecutionLifetimeManaged { + break + } + select { + case <-ctx.Done(): + t.Fatal("automation capabilities were not updated after reconnect") + case <-time.After(time.Millisecond): + } + } +} + // TestSingleHostHandshake exercises the [Single] one-line constructor // against a fake server and confirms the host transitions to the // Connected state with a populated protocol version. diff --git a/clients/go/ahp/multi_host_state_mirror.go b/clients/go/ahp/multi_host_state_mirror.go index 662d57a83..b6ef4a300 100644 --- a/clients/go/ahp/multi_host_state_mirror.go +++ b/clients/go/ahp/multi_host_state_mirror.go @@ -25,22 +25,26 @@ type HostedResourceKey struct { // [ApplyActionToRoot] / [ApplyActionToSession] / [ApplyActionToChat] / // [ApplyActionToTerminal] reducer and re-storing the result. type MultiHostStateMirror struct { - mu sync.RWMutex - roots map[string]ahptypes.RootState - session map[HostedResourceKey]ahptypes.SessionState - chat map[HostedResourceKey]ahptypes.ChatState - term map[HostedResourceKey]ahptypes.TerminalState - changes map[HostedResourceKey]ahptypes.ChangesetState + mu sync.RWMutex + roots map[string]ahptypes.RootState + session map[HostedResourceKey]ahptypes.SessionState + chat map[HostedResourceKey]ahptypes.ChatState + term map[HostedResourceKey]ahptypes.TerminalState + changes map[HostedResourceKey]ahptypes.ChangesetState + automation map[HostedResourceKey]ahptypes.AutomationState + automationRun map[HostedResourceKey]ahptypes.AutomationRunState } // NewMultiHostStateMirror returns an empty mirror. func NewMultiHostStateMirror() *MultiHostStateMirror { return &MultiHostStateMirror{ - roots: make(map[string]ahptypes.RootState), - session: make(map[HostedResourceKey]ahptypes.SessionState), - chat: make(map[HostedResourceKey]ahptypes.ChatState), - term: make(map[HostedResourceKey]ahptypes.TerminalState), - changes: make(map[HostedResourceKey]ahptypes.ChangesetState), + roots: make(map[string]ahptypes.RootState), + session: make(map[HostedResourceKey]ahptypes.SessionState), + chat: make(map[HostedResourceKey]ahptypes.ChatState), + term: make(map[HostedResourceKey]ahptypes.TerminalState), + changes: make(map[HostedResourceKey]ahptypes.ChangesetState), + automation: make(map[HostedResourceKey]ahptypes.AutomationState), + automationRun: make(map[HostedResourceKey]ahptypes.AutomationRunState), } } @@ -124,6 +128,36 @@ func (m *MultiHostStateMirror) Changeset(hostID string, uri ahptypes.URI) (ahpty return v, ok } +// PutAutomation stores an automation snapshot under (hostID, uri). +func (m *MultiHostStateMirror) PutAutomation(hostID string, uri ahptypes.URI, automation ahptypes.AutomationState) { + m.mu.Lock() + defer m.mu.Unlock() + m.automation[HostedResourceKey{hostID, uri}] = automation +} + +// Automation returns the automation snapshot at (hostID, uri). +func (m *MultiHostStateMirror) Automation(hostID string, uri ahptypes.URI) (ahptypes.AutomationState, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + v, ok := m.automation[HostedResourceKey{hostID, uri}] + return v, ok +} + +// PutAutomationRun stores an automation-run snapshot under (hostID, uri). +func (m *MultiHostStateMirror) PutAutomationRun(hostID string, uri ahptypes.URI, run ahptypes.AutomationRunState) { + m.mu.Lock() + defer m.mu.Unlock() + m.automationRun[HostedResourceKey{hostID, uri}] = run +} + +// AutomationRun returns the automation-run snapshot at (hostID, uri). +func (m *MultiHostStateMirror) AutomationRun(hostID string, uri ahptypes.URI) (ahptypes.AutomationRunState, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + v, ok := m.automationRun[HostedResourceKey{hostID, uri}] + return v, ok +} + // DropHost removes every snapshot belonging to hostID. Use when a // host is removed from the multi-host registry. func (m *MultiHostStateMirror) DropHost(hostID string) { @@ -150,6 +184,16 @@ func (m *MultiHostStateMirror) DropHost(hostID string) { delete(m.changes, k) } } + for k := range m.automation { + if k.HostID == hostID { + delete(m.automation, k) + } + } + for k := range m.automationRun { + if k.HostID == hostID { + delete(m.automationRun, k) + } + } } // DropResource removes the snapshot at (hostID, uri) across every @@ -162,4 +206,6 @@ func (m *MultiHostStateMirror) DropResource(hostID string, uri ahptypes.URI) { delete(m.chat, k) delete(m.term, k) delete(m.changes, k) + delete(m.automation, k) + delete(m.automationRun, k) } diff --git a/clients/go/ahp/multi_host_state_mirror_test.go b/clients/go/ahp/multi_host_state_mirror_test.go new file mode 100644 index 000000000..49de231ed --- /dev/null +++ b/clients/go/ahp/multi_host_state_mirror_test.go @@ -0,0 +1,52 @@ +package ahp + +import ( + "testing" + + "github.com/microsoft/agent-host-protocol/clients/go/ahptypes" +) + +func TestMultiHostStateMirrorDropHostWithoutChangesets(t *testing.T) { + mirror := NewMultiHostStateMirror() + automationURI := ahptypes.URI("ahp-automation:/nightly") + runURI := ahptypes.URI("ahp-automation-run:/nightly/1") + + mirror.PutAutomation("removed", automationURI, ahptypes.AutomationState{Resource: automationURI}) + mirror.PutAutomationRun("removed", runURI, ahptypes.AutomationRunState{Resource: runURI}) + mirror.PutAutomation("retained", automationURI, ahptypes.AutomationState{Resource: automationURI}) + mirror.PutAutomationRun("retained", runURI, ahptypes.AutomationRunState{Resource: runURI}) + + mirror.DropHost("removed") + + if _, ok := mirror.Automation("removed", automationURI); ok { + t.Error("automation for dropped host was retained") + } + if _, ok := mirror.AutomationRun("removed", runURI); ok { + t.Error("automation run for dropped host was retained") + } + if _, ok := mirror.Automation("retained", automationURI); !ok { + t.Error("automation for other host was removed") + } + if _, ok := mirror.AutomationRun("retained", runURI); !ok { + t.Error("automation run for other host was removed") + } +} + +func TestMultiHostStateMirrorDropResourceRemovesAutomationState(t *testing.T) { + mirror := NewMultiHostStateMirror() + automationURI := ahptypes.URI("ahp-automation:/nightly") + runURI := ahptypes.URI("ahp-automation-run:/nightly/1") + + mirror.PutAutomation("host", automationURI, ahptypes.AutomationState{Resource: automationURI}) + mirror.PutAutomationRun("host", runURI, ahptypes.AutomationRunState{Resource: runURI}) + + mirror.DropResource("host", automationURI) + mirror.DropResource("host", runURI) + + if _, ok := mirror.Automation("host", automationURI); ok { + t.Error("automation for dropped resource was retained") + } + if _, ok := mirror.AutomationRun("host", runURI); ok { + t.Error("automation run for dropped resource was retained") + } +} diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 9d2a34161..9b3163fc9 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -1795,3 +1795,100 @@ func ApplyActionToResourceWatch(state *ahptypes.ResourceWatchState, action ahpty } return ReduceOutcomeOutOfScope } + +// ApplyActionToAutomation applies an action to automation state. +func ApplyActionToAutomation(state *ahptypes.AutomationState, action ahptypes.StateAction) ReduceOutcome { + switch a := action.Value.(type) { + case *ahptypes.AutomationDefinitionChangedAction: + state.Definition = a.Definition + state.Revision = a.Revision + state.ModifiedAt = a.ModifiedAt + state.NextRunAt = a.NextRunAt + return ReduceOutcomeApplied + case *ahptypes.AutomationRunSummarySetAction: + for i := range state.Runs { + if state.Runs[i].Resource == a.Run.Resource { + state.Runs[i] = a.Run + return ReduceOutcomeApplied + } + } + state.Runs = append([]ahptypes.AutomationRunSummary{a.Run}, state.Runs...) + return ReduceOutcomeApplied + case *ahptypes.AutomationRunSummaryRemovedAction: + for i := range state.Runs { + if state.Runs[i].Resource == a.Run { + state.Runs = append(state.Runs[:i], state.Runs[i+1:]...) + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp + case *ahptypes.AutomationRunsLoadedAction: + known := make(map[ahptypes.URI]struct{}, len(state.Runs)) + for _, run := range state.Runs { + known[run.Resource] = struct{}{} + } + for _, run := range a.Runs { + if _, ok := known[run.Resource]; ok { + continue + } + state.Runs = append(state.Runs, run) + known[run.Resource] = struct{}{} + } + state.RunsNextCursor = a.NextCursor + return ReduceOutcomeApplied + } + return ReduceOutcomeOutOfScope +} + +// ApplyActionToAutomationRun applies an action to automation-run state. +func ApplyActionToAutomationRun(state *ahptypes.AutomationRunState, action ahptypes.StateAction) ReduceOutcome { + switch a := action.Value.(type) { + case *ahptypes.AutomationRunLifecycleChangedAction: + state.Lifecycle = a.Lifecycle + state.Operations = a.Operations + return ReduceOutcomeApplied + case *ahptypes.AutomationRunSessionSetAction: + for _, session := range state.Sessions { + if session == a.Session { + return ReduceOutcomeNoOp + } + } + state.Sessions = append(state.Sessions, a.Session) + return ReduceOutcomeApplied + case *ahptypes.AutomationRunSessionRemovedAction: + for i, session := range state.Sessions { + if session != a.Session { + continue + } + state.Sessions = append(state.Sessions[:i], state.Sessions[i+1:]...) + if state.PrimarySession != nil && *state.PrimarySession == a.Session { + state.PrimarySession = nil + } + return ReduceOutcomeApplied + } + return ReduceOutcomeNoOp + case *ahptypes.AutomationRunPrimarySessionChangedAction: + state.PrimarySession = a.PrimarySession + return ReduceOutcomeApplied + case *ahptypes.AutomationRunArtifactSetAction: + for i := range state.Artifacts { + if state.Artifacts[i].Id == a.Artifact.Id { + state.Artifacts[i] = a.Artifact + return ReduceOutcomeApplied + } + } + state.Artifacts = append(state.Artifacts, a.Artifact) + return ReduceOutcomeApplied + case *ahptypes.AutomationRunArtifactRemovedAction: + for i := range state.Artifacts { + if state.Artifacts[i].Id == a.ArtifactId { + state.Artifacts = append(state.Artifacts[:i], state.Artifacts[i+1:]...) + return ReduceOutcomeApplied + } + } + return ReduceOutcomeNoOp + case *ahptypes.AutomationRunCancelRequestedAction: + return ReduceOutcomeNoOp + } + return ReduceOutcomeOutOfScope +} diff --git a/clients/go/ahp/reducers_fixture_test.go b/clients/go/ahp/reducers_fixture_test.go index 9333353c4..da0d55aee 100644 --- a/clients/go/ahp/reducers_fixture_test.go +++ b/clients/go/ahp/reducers_fixture_test.go @@ -160,6 +160,10 @@ func TestFixtureDrivenReducerParity(t *testing.T) { runFixture[ahptypes.AnnotationsState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToAnnotations) case "resourceWatch": runFixture[ahptypes.ResourceWatchState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToResourceWatch) + case "automation": + runFixture[ahptypes.AutomationState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToAutomation) + case "automationRun": + runFixture[ahptypes.AutomationRunState](tt, fixture.Initial, fixture.Expected, actions, ApplyActionToAutomationRun) default: tt.Fatalf("unknown reducer kind %q", fixture.Reducer) } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index fccdb904b..17f33c315 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -19,91 +19,102 @@ var _ = json.RawMessage(nil) type ActionType string const ( - ActionTypeRootAgentsChanged ActionType = "root/agentsChanged" - ActionTypeRootActiveSessionsChanged ActionType = "root/activeSessionsChanged" - ActionTypeSessionReady ActionType = "session/ready" - ActionTypeSessionCreationFailed ActionType = "session/creationFailed" - ActionTypeSessionChatAdded ActionType = "session/chatAdded" - ActionTypeSessionChatRemoved ActionType = "session/chatRemoved" - ActionTypeSessionChatUpdated ActionType = "session/chatUpdated" - ActionTypeSessionDefaultChatChanged ActionType = "session/defaultChatChanged" - ActionTypeChatTurnStarted ActionType = "chat/turnStarted" - ActionTypeChatDelta ActionType = "chat/delta" - ActionTypeChatResponsePart ActionType = "chat/responsePart" - ActionTypeChatToolCallStart ActionType = "chat/toolCallStart" - ActionTypeChatToolCallDelta ActionType = "chat/toolCallDelta" - ActionTypeChatToolCallReady ActionType = "chat/toolCallReady" - ActionTypeChatToolCallConfirmed ActionType = "chat/toolCallConfirmed" - ActionTypeChatToolCallComplete ActionType = "chat/toolCallComplete" - ActionTypeChatToolCallResultConfirmed ActionType = "chat/toolCallResultConfirmed" - ActionTypeChatToolCallContentChanged ActionType = "chat/toolCallContentChanged" - ActionTypeChatToolCallAuthRequired ActionType = "chat/toolCallAuthRequired" - ActionTypeChatToolCallAuthResolved ActionType = "chat/toolCallAuthResolved" - ActionTypeChatTurnComplete ActionType = "chat/turnComplete" - ActionTypeChatTurnCancelled ActionType = "chat/turnCancelled" - ActionTypeChatError ActionType = "chat/error" - ActionTypeChatActivityChanged ActionType = "chat/activityChanged" - ActionTypeChatWorkingDirectorySet ActionType = "chat/workingDirectorySet" - ActionTypeChatWorkingDirectoryRemoved ActionType = "chat/workingDirectoryRemoved" - ActionTypeSessionTitleChanged ActionType = "session/titleChanged" - ActionTypeChatUsage ActionType = "chat/usage" - ActionTypeChatReasoning ActionType = "chat/reasoning" - ActionTypeSessionServerToolsChanged ActionType = "session/serverToolsChanged" - ActionTypeSessionActiveClientSet ActionType = "session/activeClientSet" - ActionTypeSessionActiveClientRemoved ActionType = "session/activeClientRemoved" - ActionTypeSessionWorkingDirectorySet ActionType = "session/workingDirectorySet" - ActionTypeSessionWorkingDirectoryRemoved ActionType = "session/workingDirectoryRemoved" - ActionTypeSessionInputNeededSet ActionType = "session/inputNeededSet" - ActionTypeSessionInputNeededRemoved ActionType = "session/inputNeededRemoved" - ActionTypeChatPendingMessageSet ActionType = "chat/pendingMessageSet" - ActionTypeChatPendingMessageRemoved ActionType = "chat/pendingMessageRemoved" - ActionTypeChatQueuedMessagesReordered ActionType = "chat/queuedMessagesReordered" - ActionTypeChatDraftChanged ActionType = "chat/draftChanged" - ActionTypeChatInputRequested ActionType = "chat/inputRequested" - ActionTypeChatInputAnswerChanged ActionType = "chat/inputAnswerChanged" - ActionTypeChatInputCompleted ActionType = "chat/inputCompleted" - ActionTypeSessionCustomizationsChanged ActionType = "session/customizationsChanged" - ActionTypeSessionCustomizationToggled ActionType = "session/customizationToggled" - ActionTypeSessionCustomizationUpdated ActionType = "session/customizationUpdated" - ActionTypeSessionCustomizationRemoved ActionType = "session/customizationRemoved" - ActionTypeSessionMcpServerStateChanged ActionType = "session/mcpServerStateChanged" - ActionTypeSessionMcpServerStartRequested ActionType = "session/mcpServerStartRequested" - ActionTypeSessionMcpServerStopRequested ActionType = "session/mcpServerStopRequested" - ActionTypeChatTruncated ActionType = "chat/truncated" - ActionTypeChatTurnsLoaded ActionType = "chat/turnsLoaded" - ActionTypeSessionIsReadChanged ActionType = "session/isReadChanged" - ActionTypeSessionIsArchivedChanged ActionType = "session/isArchivedChanged" - ActionTypeSessionActivityChanged ActionType = "session/activityChanged" - ActionTypeSessionChangesetsChanged ActionType = "session/changesetsChanged" - ActionTypeSessionConfigChanged ActionType = "session/configChanged" - ActionTypeSessionMetaChanged ActionType = "session/metaChanged" - ActionTypeChangesetStatusChanged ActionType = "changeset/statusChanged" - ActionTypeChangesetFileSet ActionType = "changeset/fileSet" - ActionTypeChangesetFileRemoved ActionType = "changeset/fileRemoved" - ActionTypeChangesetFilesReviewChanged ActionType = "changeset/filesReviewChanged" - ActionTypeChangesetContentChanged ActionType = "changeset/contentChanged" - ActionTypeChangesetOperationsChanged ActionType = "changeset/operationsChanged" - ActionTypeChangesetOperationStatusChanged ActionType = "changeset/operationStatusChanged" - ActionTypeChangesetCleared ActionType = "changeset/cleared" - ActionTypeAnnotationsSet ActionType = "annotations/set" - ActionTypeAnnotationsUpdated ActionType = "annotations/updated" - ActionTypeAnnotationsRemoved ActionType = "annotations/removed" - ActionTypeAnnotationsEntrySet ActionType = "annotations/entrySet" - ActionTypeAnnotationsEntryRemoved ActionType = "annotations/entryRemoved" - ActionTypeRootTerminalsChanged ActionType = "root/terminalsChanged" - ActionTypeRootConfigChanged ActionType = "root/configChanged" - ActionTypeTerminalData ActionType = "terminal/data" - ActionTypeTerminalInput ActionType = "terminal/input" - ActionTypeTerminalResized ActionType = "terminal/resized" - ActionTypeTerminalClaimed ActionType = "terminal/claimed" - ActionTypeTerminalTitleChanged ActionType = "terminal/titleChanged" - ActionTypeTerminalCwdChanged ActionType = "terminal/cwdChanged" - ActionTypeTerminalExited ActionType = "terminal/exited" - ActionTypeTerminalCleared ActionType = "terminal/cleared" - ActionTypeTerminalCommandDetectionAvailable ActionType = "terminal/commandDetectionAvailable" - ActionTypeTerminalCommandExecuted ActionType = "terminal/commandExecuted" - ActionTypeTerminalCommandFinished ActionType = "terminal/commandFinished" - ActionTypeResourceWatchChanged ActionType = "resourceWatch/changed" + ActionTypeRootAgentsChanged ActionType = "root/agentsChanged" + ActionTypeRootActiveSessionsChanged ActionType = "root/activeSessionsChanged" + ActionTypeSessionReady ActionType = "session/ready" + ActionTypeSessionCreationFailed ActionType = "session/creationFailed" + ActionTypeSessionChatAdded ActionType = "session/chatAdded" + ActionTypeSessionChatRemoved ActionType = "session/chatRemoved" + ActionTypeSessionChatUpdated ActionType = "session/chatUpdated" + ActionTypeSessionDefaultChatChanged ActionType = "session/defaultChatChanged" + ActionTypeChatTurnStarted ActionType = "chat/turnStarted" + ActionTypeChatDelta ActionType = "chat/delta" + ActionTypeChatResponsePart ActionType = "chat/responsePart" + ActionTypeChatToolCallStart ActionType = "chat/toolCallStart" + ActionTypeChatToolCallDelta ActionType = "chat/toolCallDelta" + ActionTypeChatToolCallReady ActionType = "chat/toolCallReady" + ActionTypeChatToolCallConfirmed ActionType = "chat/toolCallConfirmed" + ActionTypeChatToolCallComplete ActionType = "chat/toolCallComplete" + ActionTypeChatToolCallResultConfirmed ActionType = "chat/toolCallResultConfirmed" + ActionTypeChatToolCallContentChanged ActionType = "chat/toolCallContentChanged" + ActionTypeChatToolCallAuthRequired ActionType = "chat/toolCallAuthRequired" + ActionTypeChatToolCallAuthResolved ActionType = "chat/toolCallAuthResolved" + ActionTypeChatTurnComplete ActionType = "chat/turnComplete" + ActionTypeChatTurnCancelled ActionType = "chat/turnCancelled" + ActionTypeChatError ActionType = "chat/error" + ActionTypeChatActivityChanged ActionType = "chat/activityChanged" + ActionTypeChatWorkingDirectorySet ActionType = "chat/workingDirectorySet" + ActionTypeChatWorkingDirectoryRemoved ActionType = "chat/workingDirectoryRemoved" + ActionTypeSessionTitleChanged ActionType = "session/titleChanged" + ActionTypeChatUsage ActionType = "chat/usage" + ActionTypeChatReasoning ActionType = "chat/reasoning" + ActionTypeSessionServerToolsChanged ActionType = "session/serverToolsChanged" + ActionTypeSessionActiveClientSet ActionType = "session/activeClientSet" + ActionTypeSessionActiveClientRemoved ActionType = "session/activeClientRemoved" + ActionTypeSessionWorkingDirectorySet ActionType = "session/workingDirectorySet" + ActionTypeSessionWorkingDirectoryRemoved ActionType = "session/workingDirectoryRemoved" + ActionTypeSessionInputNeededSet ActionType = "session/inputNeededSet" + ActionTypeSessionInputNeededRemoved ActionType = "session/inputNeededRemoved" + ActionTypeChatPendingMessageSet ActionType = "chat/pendingMessageSet" + ActionTypeChatPendingMessageRemoved ActionType = "chat/pendingMessageRemoved" + ActionTypeChatQueuedMessagesReordered ActionType = "chat/queuedMessagesReordered" + ActionTypeChatDraftChanged ActionType = "chat/draftChanged" + ActionTypeChatInputRequested ActionType = "chat/inputRequested" + ActionTypeChatInputAnswerChanged ActionType = "chat/inputAnswerChanged" + ActionTypeChatInputCompleted ActionType = "chat/inputCompleted" + ActionTypeSessionCustomizationsChanged ActionType = "session/customizationsChanged" + ActionTypeSessionCustomizationToggled ActionType = "session/customizationToggled" + ActionTypeSessionCustomizationUpdated ActionType = "session/customizationUpdated" + ActionTypeSessionCustomizationRemoved ActionType = "session/customizationRemoved" + ActionTypeSessionMcpServerStateChanged ActionType = "session/mcpServerStateChanged" + ActionTypeSessionMcpServerStartRequested ActionType = "session/mcpServerStartRequested" + ActionTypeSessionMcpServerStopRequested ActionType = "session/mcpServerStopRequested" + ActionTypeChatTruncated ActionType = "chat/truncated" + ActionTypeChatTurnsLoaded ActionType = "chat/turnsLoaded" + ActionTypeSessionIsReadChanged ActionType = "session/isReadChanged" + ActionTypeSessionIsArchivedChanged ActionType = "session/isArchivedChanged" + ActionTypeSessionActivityChanged ActionType = "session/activityChanged" + ActionTypeSessionChangesetsChanged ActionType = "session/changesetsChanged" + ActionTypeSessionConfigChanged ActionType = "session/configChanged" + ActionTypeSessionMetaChanged ActionType = "session/metaChanged" + ActionTypeChangesetStatusChanged ActionType = "changeset/statusChanged" + ActionTypeChangesetFileSet ActionType = "changeset/fileSet" + ActionTypeChangesetFileRemoved ActionType = "changeset/fileRemoved" + ActionTypeChangesetFilesReviewChanged ActionType = "changeset/filesReviewChanged" + ActionTypeChangesetContentChanged ActionType = "changeset/contentChanged" + ActionTypeChangesetOperationsChanged ActionType = "changeset/operationsChanged" + ActionTypeChangesetOperationStatusChanged ActionType = "changeset/operationStatusChanged" + ActionTypeChangesetCleared ActionType = "changeset/cleared" + ActionTypeAnnotationsSet ActionType = "annotations/set" + ActionTypeAnnotationsUpdated ActionType = "annotations/updated" + ActionTypeAnnotationsRemoved ActionType = "annotations/removed" + ActionTypeAnnotationsEntrySet ActionType = "annotations/entrySet" + ActionTypeAnnotationsEntryRemoved ActionType = "annotations/entryRemoved" + ActionTypeRootTerminalsChanged ActionType = "root/terminalsChanged" + ActionTypeRootConfigChanged ActionType = "root/configChanged" + ActionTypeTerminalData ActionType = "terminal/data" + ActionTypeTerminalInput ActionType = "terminal/input" + ActionTypeTerminalResized ActionType = "terminal/resized" + ActionTypeTerminalClaimed ActionType = "terminal/claimed" + ActionTypeTerminalTitleChanged ActionType = "terminal/titleChanged" + ActionTypeTerminalCwdChanged ActionType = "terminal/cwdChanged" + ActionTypeTerminalExited ActionType = "terminal/exited" + ActionTypeTerminalCleared ActionType = "terminal/cleared" + ActionTypeTerminalCommandDetectionAvailable ActionType = "terminal/commandDetectionAvailable" + ActionTypeTerminalCommandExecuted ActionType = "terminal/commandExecuted" + ActionTypeTerminalCommandFinished ActionType = "terminal/commandFinished" + ActionTypeResourceWatchChanged ActionType = "resourceWatch/changed" + ActionTypeAutomationDefinitionChanged ActionType = "automation/definitionChanged" + ActionTypeAutomationRunSummarySet ActionType = "automation/runSummarySet" + ActionTypeAutomationRunSummaryRemoved ActionType = "automation/runSummaryRemoved" + ActionTypeAutomationRunsLoaded ActionType = "automation/runsLoaded" + ActionTypeAutomationRunLifecycleChanged ActionType = "automationRun/lifecycleChanged" + ActionTypeAutomationRunSessionSet ActionType = "automationRun/sessionSet" + ActionTypeAutomationRunSessionRemoved ActionType = "automationRun/sessionRemoved" + ActionTypeAutomationRunPrimarySessionChanged ActionType = "automationRun/primarySessionChanged" + ActionTypeAutomationRunArtifactSet ActionType = "automationRun/artifactSet" + ActionTypeAutomationRunArtifactRemoved ActionType = "automationRun/artifactRemoved" + ActionTypeAutomationRunCancelRequested ActionType = "automationRun/cancelRequested" ) // ─── Action Envelope ───────────────────────────────────────────────── @@ -1480,6 +1491,65 @@ type ResourceWatchChangedAction struct { Changes json.RawMessage `json:"changes"` } +type AutomationDefinitionChangedAction struct { + Type ActionType `json:"type"` + Definition AutomationDefinition `json:"definition"` + Revision int64 `json:"revision"` + ModifiedAt string `json:"modifiedAt"` + NextRunAt *string `json:"nextRunAt,omitempty"` +} + +type AutomationRunSummarySetAction struct { + Type ActionType `json:"type"` + Run AutomationRunSummary `json:"run"` +} + +type AutomationRunSummaryRemovedAction struct { + Type ActionType `json:"type"` + Run URI `json:"run"` +} + +type AutomationRunsLoadedAction struct { + Type ActionType `json:"type"` + Runs []AutomationRunSummary `json:"runs"` + NextCursor *string `json:"nextCursor,omitempty"` +} + +type AutomationRunLifecycleChangedAction struct { + Type ActionType `json:"type"` + Lifecycle AutomationRunLifecycle `json:"lifecycle"` + Operations []AutomationRunOperation `json:"operations"` +} + +type AutomationRunSessionSetAction struct { + Type ActionType `json:"type"` + Session URI `json:"session"` +} + +type AutomationRunSessionRemovedAction struct { + Type ActionType `json:"type"` + Session URI `json:"session"` +} + +type AutomationRunPrimarySessionChangedAction struct { + Type ActionType `json:"type"` + PrimarySession *URI `json:"primarySession,omitempty"` +} + +type AutomationRunArtifactSetAction struct { + Type ActionType `json:"type"` + Artifact AutomationRunArtifact `json:"artifact"` +} + +type AutomationRunArtifactRemovedAction struct { + Type ActionType `json:"type"` + ArtifactId string `json:"artifactId"` +} + +type AutomationRunCancelRequestedAction struct { + Type ActionType `json:"type"` +} + // ─── StateAction Union ─────────────────────────────────────────────── // StateAction is the discriminated union of every state action. @@ -1491,91 +1561,102 @@ type StateAction struct { // concrete variant of StateAction. type isStateAction interface{ isStateAction() } -func (*RootAgentsChangedAction) isStateAction() {} -func (*RootActiveSessionsChangedAction) isStateAction() {} -func (*RootConfigChangedAction) isStateAction() {} -func (*SessionReadyAction) isStateAction() {} -func (*SessionCreationFailedAction) isStateAction() {} -func (*SessionChatAddedAction) isStateAction() {} -func (*SessionChatRemovedAction) isStateAction() {} -func (*SessionChatUpdatedAction) isStateAction() {} -func (*SessionDefaultChatChangedAction) isStateAction() {} -func (*ChatTurnStartedAction) isStateAction() {} -func (*ChatDeltaAction) isStateAction() {} -func (*ChatResponsePartAction) isStateAction() {} -func (*ChatToolCallStartAction) isStateAction() {} -func (*ChatToolCallDeltaAction) isStateAction() {} -func (*ChatToolCallReadyAction) isStateAction() {} -func (*ChatToolCallConfirmedAction) isStateAction() {} -func (*ChatToolCallCompleteAction) isStateAction() {} -func (*ChatToolCallResultConfirmedAction) isStateAction() {} -func (*ChatToolCallContentChangedAction) isStateAction() {} -func (*ChatToolCallAuthRequiredAction) isStateAction() {} -func (*ChatToolCallAuthResolvedAction) isStateAction() {} -func (*ChatTurnCompleteAction) isStateAction() {} -func (*ChatTurnCancelledAction) isStateAction() {} -func (*ChatErrorAction) isStateAction() {} -func (*ChatActivityChangedAction) isStateAction() {} -func (*SessionTitleChangedAction) isStateAction() {} -func (*ChatUsageAction) isStateAction() {} -func (*ChatReasoningAction) isStateAction() {} -func (*ChatPendingMessageSetAction) isStateAction() {} -func (*ChatPendingMessageRemovedAction) isStateAction() {} -func (*ChatQueuedMessagesReorderedAction) isStateAction() {} -func (*ChatDraftChangedAction) isStateAction() {} -func (*ChatInputRequestedAction) isStateAction() {} -func (*ChatInputAnswerChangedAction) isStateAction() {} -func (*ChatInputCompletedAction) isStateAction() {} -func (*ChatTruncatedAction) isStateAction() {} -func (*ChatTurnsLoadedAction) isStateAction() {} -func (*SessionIsReadChangedAction) isStateAction() {} -func (*SessionIsArchivedChangedAction) isStateAction() {} -func (*SessionActivityChangedAction) isStateAction() {} -func (*SessionChangesetsChangedAction) isStateAction() {} -func (*SessionServerToolsChangedAction) isStateAction() {} -func (*SessionActiveClientSetAction) isStateAction() {} -func (*SessionActiveClientRemovedAction) isStateAction() {} -func (*SessionWorkingDirectorySetAction) isStateAction() {} -func (*SessionWorkingDirectoryRemovedAction) isStateAction() {} -func (*ChatWorkingDirectorySetAction) isStateAction() {} -func (*ChatWorkingDirectoryRemovedAction) isStateAction() {} -func (*SessionInputNeededSetAction) isStateAction() {} -func (*SessionInputNeededRemovedAction) isStateAction() {} -func (*SessionCustomizationsChangedAction) isStateAction() {} -func (*SessionCustomizationToggledAction) isStateAction() {} -func (*SessionCustomizationUpdatedAction) isStateAction() {} -func (*SessionCustomizationRemovedAction) isStateAction() {} -func (*SessionMcpServerStateChangedAction) isStateAction() {} -func (*SessionMcpServerStartRequestedAction) isStateAction() {} -func (*SessionMcpServerStopRequestedAction) isStateAction() {} -func (*SessionConfigChangedAction) isStateAction() {} -func (*SessionMetaChangedAction) isStateAction() {} -func (*ChangesetStatusChangedAction) isStateAction() {} -func (*ChangesetFileSetAction) isStateAction() {} -func (*ChangesetFileRemovedAction) isStateAction() {} -func (*ChangesetFilesReviewChangedAction) isStateAction() {} -func (*ChangesetContentChangedAction) isStateAction() {} -func (*ChangesetOperationsChangedAction) isStateAction() {} -func (*ChangesetOperationStatusChangedAction) isStateAction() {} -func (*ChangesetClearedAction) isStateAction() {} -func (*AnnotationsSetAction) isStateAction() {} -func (*AnnotationsUpdatedAction) isStateAction() {} -func (*AnnotationsRemovedAction) isStateAction() {} -func (*AnnotationsEntrySetAction) isStateAction() {} -func (*AnnotationsEntryRemovedAction) isStateAction() {} -func (*RootTerminalsChangedAction) isStateAction() {} -func (*TerminalDataAction) isStateAction() {} -func (*TerminalInputAction) isStateAction() {} -func (*TerminalResizedAction) isStateAction() {} -func (*TerminalClaimedAction) isStateAction() {} -func (*TerminalTitleChangedAction) isStateAction() {} -func (*TerminalCwdChangedAction) isStateAction() {} -func (*TerminalExitedAction) isStateAction() {} -func (*TerminalClearedAction) isStateAction() {} -func (*TerminalCommandDetectionAvailableAction) isStateAction() {} -func (*TerminalCommandExecutedAction) isStateAction() {} -func (*TerminalCommandFinishedAction) isStateAction() {} -func (*ResourceWatchChangedAction) isStateAction() {} +func (*RootAgentsChangedAction) isStateAction() {} +func (*RootActiveSessionsChangedAction) isStateAction() {} +func (*RootConfigChangedAction) isStateAction() {} +func (*SessionReadyAction) isStateAction() {} +func (*SessionCreationFailedAction) isStateAction() {} +func (*SessionChatAddedAction) isStateAction() {} +func (*SessionChatRemovedAction) isStateAction() {} +func (*SessionChatUpdatedAction) isStateAction() {} +func (*SessionDefaultChatChangedAction) isStateAction() {} +func (*ChatTurnStartedAction) isStateAction() {} +func (*ChatDeltaAction) isStateAction() {} +func (*ChatResponsePartAction) isStateAction() {} +func (*ChatToolCallStartAction) isStateAction() {} +func (*ChatToolCallDeltaAction) isStateAction() {} +func (*ChatToolCallReadyAction) isStateAction() {} +func (*ChatToolCallConfirmedAction) isStateAction() {} +func (*ChatToolCallCompleteAction) isStateAction() {} +func (*ChatToolCallResultConfirmedAction) isStateAction() {} +func (*ChatToolCallContentChangedAction) isStateAction() {} +func (*ChatToolCallAuthRequiredAction) isStateAction() {} +func (*ChatToolCallAuthResolvedAction) isStateAction() {} +func (*ChatTurnCompleteAction) isStateAction() {} +func (*ChatTurnCancelledAction) isStateAction() {} +func (*ChatErrorAction) isStateAction() {} +func (*ChatActivityChangedAction) isStateAction() {} +func (*SessionTitleChangedAction) isStateAction() {} +func (*ChatUsageAction) isStateAction() {} +func (*ChatReasoningAction) isStateAction() {} +func (*ChatPendingMessageSetAction) isStateAction() {} +func (*ChatPendingMessageRemovedAction) isStateAction() {} +func (*ChatQueuedMessagesReorderedAction) isStateAction() {} +func (*ChatDraftChangedAction) isStateAction() {} +func (*ChatInputRequestedAction) isStateAction() {} +func (*ChatInputAnswerChangedAction) isStateAction() {} +func (*ChatInputCompletedAction) isStateAction() {} +func (*ChatTruncatedAction) isStateAction() {} +func (*ChatTurnsLoadedAction) isStateAction() {} +func (*SessionIsReadChangedAction) isStateAction() {} +func (*SessionIsArchivedChangedAction) isStateAction() {} +func (*SessionActivityChangedAction) isStateAction() {} +func (*SessionChangesetsChangedAction) isStateAction() {} +func (*SessionServerToolsChangedAction) isStateAction() {} +func (*SessionActiveClientSetAction) isStateAction() {} +func (*SessionActiveClientRemovedAction) isStateAction() {} +func (*SessionWorkingDirectorySetAction) isStateAction() {} +func (*SessionWorkingDirectoryRemovedAction) isStateAction() {} +func (*ChatWorkingDirectorySetAction) isStateAction() {} +func (*ChatWorkingDirectoryRemovedAction) isStateAction() {} +func (*SessionInputNeededSetAction) isStateAction() {} +func (*SessionInputNeededRemovedAction) isStateAction() {} +func (*SessionCustomizationsChangedAction) isStateAction() {} +func (*SessionCustomizationToggledAction) isStateAction() {} +func (*SessionCustomizationUpdatedAction) isStateAction() {} +func (*SessionCustomizationRemovedAction) isStateAction() {} +func (*SessionMcpServerStateChangedAction) isStateAction() {} +func (*SessionMcpServerStartRequestedAction) isStateAction() {} +func (*SessionMcpServerStopRequestedAction) isStateAction() {} +func (*SessionConfigChangedAction) isStateAction() {} +func (*SessionMetaChangedAction) isStateAction() {} +func (*ChangesetStatusChangedAction) isStateAction() {} +func (*ChangesetFileSetAction) isStateAction() {} +func (*ChangesetFileRemovedAction) isStateAction() {} +func (*ChangesetFilesReviewChangedAction) isStateAction() {} +func (*ChangesetContentChangedAction) isStateAction() {} +func (*ChangesetOperationsChangedAction) isStateAction() {} +func (*ChangesetOperationStatusChangedAction) isStateAction() {} +func (*ChangesetClearedAction) isStateAction() {} +func (*AnnotationsSetAction) isStateAction() {} +func (*AnnotationsUpdatedAction) isStateAction() {} +func (*AnnotationsRemovedAction) isStateAction() {} +func (*AnnotationsEntrySetAction) isStateAction() {} +func (*AnnotationsEntryRemovedAction) isStateAction() {} +func (*RootTerminalsChangedAction) isStateAction() {} +func (*TerminalDataAction) isStateAction() {} +func (*TerminalInputAction) isStateAction() {} +func (*TerminalResizedAction) isStateAction() {} +func (*TerminalClaimedAction) isStateAction() {} +func (*TerminalTitleChangedAction) isStateAction() {} +func (*TerminalCwdChangedAction) isStateAction() {} +func (*TerminalExitedAction) isStateAction() {} +func (*TerminalClearedAction) isStateAction() {} +func (*TerminalCommandDetectionAvailableAction) isStateAction() {} +func (*TerminalCommandExecutedAction) isStateAction() {} +func (*TerminalCommandFinishedAction) isStateAction() {} +func (*ResourceWatchChangedAction) isStateAction() {} +func (*AutomationDefinitionChangedAction) isStateAction() {} +func (*AutomationRunSummarySetAction) isStateAction() {} +func (*AutomationRunSummaryRemovedAction) isStateAction() {} +func (*AutomationRunsLoadedAction) isStateAction() {} +func (*AutomationRunLifecycleChangedAction) isStateAction() {} +func (*AutomationRunSessionSetAction) isStateAction() {} +func (*AutomationRunSessionRemovedAction) isStateAction() {} +func (*AutomationRunPrimarySessionChangedAction) isStateAction() {} +func (*AutomationRunArtifactSetAction) isStateAction() {} +func (*AutomationRunArtifactRemovedAction) isStateAction() {} +func (*AutomationRunCancelRequestedAction) isStateAction() {} // StateActionUnknown carries an unrecognized StateAction 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 StateActionUnknown struct { @@ -2101,6 +2182,72 @@ func (u *StateAction) UnmarshalJSON(data []byte) error { return err } u.Value = &value + case "automation/definitionChanged": + var value AutomationDefinitionChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automation/runSummarySet": + var value AutomationRunSummarySetAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automation/runSummaryRemoved": + var value AutomationRunSummaryRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automation/runsLoaded": + var value AutomationRunsLoadedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automationRun/lifecycleChanged": + var value AutomationRunLifecycleChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automationRun/sessionSet": + var value AutomationRunSessionSetAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automationRun/sessionRemoved": + var value AutomationRunSessionRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automationRun/primarySessionChanged": + var value AutomationRunPrimarySessionChangedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automationRun/artifactSet": + var value AutomationRunArtifactSetAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automationRun/artifactRemoved": + var value AutomationRunArtifactRemovedAction + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "automationRun/cancelRequested": + var value AutomationRunCancelRequestedAction + 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/go/ahptypes/ahptypes_test.go b/clients/go/ahptypes/ahptypes_test.go index 8970c3e36..2422c27e4 100644 --- a/clients/go/ahptypes/ahptypes_test.go +++ b/clients/go/ahptypes/ahptypes_test.go @@ -143,6 +143,27 @@ func TestSnapshotStateVariants(t *testing.T) { } } +func TestAutomationScheduleUsesUnionDiscriminator(t *testing.T) { + schedule := AutomationSchedule{ + Value: &AutomationDailySchedule{ + Kind: AutomationScheduleKindHourly, + Time: AutomationLocalTime{Hour: 9, Minute: 0}, + TimeZone: "UTC", + }, + } + data, err := json.Marshal(schedule) + if err != nil { + t.Fatal(err) + } + var wire map[string]any + if err := json.Unmarshal(data, &wire); err != nil { + t.Fatal(err) + } + if wire["kind"] != "daily" { + t.Fatalf("expected daily union discriminator, got %v", wire["kind"]) + } +} + // TestSessionStatusBitset confirms the typed-uint32 Has/Or helpers // match the canonical bitset semantics. func TestSessionStatusBitset(t *testing.T) { diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index d7545ffc1..b3f19c727 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -167,6 +167,8 @@ type InitializeResult struct { // defines a template variable, `{level}`, for subscriber-side severity // filtering). Clients MAY ignore signals they cannot process. Telemetry *TelemetryCapabilities `json:"telemetry,omitempty"` + // Host automation support. Absence means unsupported. + Automations *AutomationCapabilities `json:"automations,omitempty"` } // Optional capabilities a client declares during `initialize`. @@ -189,6 +191,38 @@ type ClientCapabilities struct { McpApps map[string]json.RawMessage `json:"mcpApps,omitempty"` } +type AutomationCapabilities struct { + Execution AutomationExecutionCapabilities `json:"execution"` + Create *AutomationCreateCapability `json:"create,omitempty"` + Schedules *AutomationScheduleCapabilities `json:"schedules,omitempty"` + RunCancellation *AutomationRunCancellationCapability `json:"runCancellation,omitempty"` + SchedulePreview *AutomationSchedulePreviewCapability `json:"schedulePreview,omitempty"` + RunHistoryLimit *int64 `json:"runHistoryLimit,omitempty"` +} + +type AutomationExecutionCapabilities struct { + Lifetime AutomationExecutionLifetime `json:"lifetime"` +} + +type AutomationCreateCapability struct { +} + +type AutomationScheduleCapabilities struct { + Kinds []AutomationScheduleKind `json:"kinds"` + Cron *AutomationCronScheduleCapability `json:"cron,omitempty"` +} + +type AutomationCronScheduleCapability struct { + Dialect string `json:"dialect"` + MinIntervalMinutes *int64 `json:"minIntervalMinutes,omitempty"` +} + +type AutomationRunCancellationCapability struct { +} + +type AutomationSchedulePreviewCapability struct { +} + // Identifies a protocol implementation — the software (and build) on one end // of the connection, as distinct from the {@link AgentInfo | agent persona} it // hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the @@ -1177,6 +1211,99 @@ type ChangesetOperationFollowUp struct { External *bool `json:"external,omitempty"` } +type ListAutomationsParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + // Maximum number of entries to return in this page. The server SHOULD respect + // this bound but MAY return fewer entries and MAY impose its own upper cap. + // Omit to let the server choose the page size. + Limit *int64 `json:"limit,omitempty"` + // Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + // Omit to fetch the first page. Cursors are server-defined and MUST be treated + // as opaque — do not parse, modify, or persist them across connections. An + // unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + Cursor *string `json:"cursor,omitempty"` + Enabled *bool `json:"enabled,omitempty"` +} + +type ListAutomationsResult struct { + // Opaque cursor for the next page. Present when more entries exist beyond the + // returned page; absent signals the end of the collection. Pass it back as + // {@link PaginatedParams.cursor} to fetch the following page. + NextCursor *string `json:"nextCursor,omitempty"` + Items []AutomationSummary `json:"items"` +} + +type ListAutomationTriggerDefinitionsParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + Provider *string `json:"provider,omitempty"` + WorkingDirectories []URI `json:"workingDirectories,omitempty"` + SessionConfig map[string]json.RawMessage `json:"sessionConfig,omitempty"` +} + +type ListAutomationTriggerDefinitionsResult struct { + Items []AutomationTriggerDefinition `json:"items"` +} + +type CreateAutomationParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + Definition AutomationDefinition `json:"definition"` + Import *json.RawMessage `json:"import,omitempty"` +} + +type AutomationDefinitionPatch struct { + Title *string `json:"title,omitempty"` + Message *Message `json:"message,omitempty"` + Session *AutomationSessionTemplate `json:"session,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Triggers *[]AutomationTrigger `json:"triggers,omitempty"` + Meta *map[string]json.RawMessage `json:"_meta,omitempty"` +} + +type UpdateAutomationParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + ExpectedRevision int64 `json:"expectedRevision"` + Changes AutomationDefinitionPatch `json:"changes"` +} + +type DisposeAutomationParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` +} + +type RunAutomationParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + RequestId string `json:"requestId"` +} + +type RunAutomationResult struct { + Run URI `json:"run"` +} + +type FetchAutomationRunsParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + Cursor *string `json:"cursor,omitempty"` +} + +type FetchAutomationRunsResult struct { +} + +type PreviewAutomationScheduleParams struct { + // Channel URI this command targets. + Channel URI `json:"channel"` + Schedule AutomationSchedule `json:"schedule"` + Count *int64 `json:"count,omitempty"` +} + +type PreviewAutomationScheduleResult struct { + Items []string `json:"items"` +} + func (v *ForkChatSource) UnmarshalJSON(data []byte) error { disc, ok, err := readDiscriminator(data, "kind") if err != nil { diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 34e0908dd..b01c55bca 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -85,6 +85,21 @@ type SessionSummaryChangedParams struct { Changes PartialSessionSummary `json:"changes"` } +type AutomationAddedParams struct { + Channel URI `json:"channel"` + Summary AutomationSummary `json:"summary"` +} + +type AutomationRemovedParams struct { + Channel URI `json:"channel"` + Automation URI `json:"automation"` +} + +type AutomationSummaryChangedParams struct { + Channel URI `json:"channel"` + Summary AutomationSummary `json:"summary"` +} + // Generic progress notification for a long-running operation. // // A client opts in to progress for a request by including a `progressToken` in @@ -215,6 +230,8 @@ type PartialSessionSummary struct { Status *SessionStatus `json:"status,omitempty"` // Human-readable description of what the session is currently doing Activity *string `json:"activity,omitempty"` + // Durable origin of this session, when another AHP resource created it. + Origin *SessionOrigin `json:"origin,omitempty"` // Server-owned project for this session Project *ProjectInfo `json:"project,omitempty"` // The working directories the session's agent has tool access to, as diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go index 834d30eef..0f7181385 100644 --- a/clients/go/ahptypes/roundtrip_fixture_test.go +++ b/clients/go/ahptypes/roundtrip_fixture_test.go @@ -246,6 +246,10 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string { var v ChatSource dec(&v) return enc(&v) + case "Snapshot": + var v Snapshot + dec(&v) + return enc(&v) default: t.Fatalf("%s: round-trip fixture: unknown wire type %q. Add a decode entry to decodeAndReencode.", name, typ) return "" diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 99693a7bb..5a97c79ca 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -442,6 +442,95 @@ const ( ResourceChangeTypeDeleted ResourceChangeType = "deleted" ) +type SessionOriginKind string + +const ( + SessionOriginKindAutomation SessionOriginKind = "automation" +) + +type AutomationOperation string + +const ( + AutomationOperationUpdate AutomationOperation = "update" + AutomationOperationDispose AutomationOperation = "dispose" + AutomationOperationRun AutomationOperation = "run" +) + +type AutomationExecutionLifetime string + +const ( + AutomationExecutionLifetimeHostLifetime AutomationExecutionLifetime = "hostLifetime" + AutomationExecutionLifetimeManaged AutomationExecutionLifetime = "managed" +) + +type AutomationScheduleKind string + +const ( + AutomationScheduleKindHourly AutomationScheduleKind = "hourly" + AutomationScheduleKindDaily AutomationScheduleKind = "daily" + AutomationScheduleKindWeekly AutomationScheduleKind = "weekly" + AutomationScheduleKindCron AutomationScheduleKind = "cron" +) + +type AutomationWeekday string + +const ( + AutomationWeekdayMonday AutomationWeekday = "monday" + AutomationWeekdayTuesday AutomationWeekday = "tuesday" + AutomationWeekdayWednesday AutomationWeekday = "wednesday" + AutomationWeekdayThursday AutomationWeekday = "thursday" + AutomationWeekdayFriday AutomationWeekday = "friday" + AutomationWeekdaySaturday AutomationWeekday = "saturday" + AutomationWeekdaySunday AutomationWeekday = "sunday" +) + +type AutomationMisfirePolicy string + +const ( + AutomationMisfirePolicySkip AutomationMisfirePolicy = "skip" + AutomationMisfirePolicyRunOnce AutomationMisfirePolicy = "runOnce" +) + +type AutomationTriggerKind string + +const ( + AutomationTriggerKindSchedule AutomationTriggerKind = "schedule" + AutomationTriggerKindEvent AutomationTriggerKind = "event" +) + +type AutomationRunStatus string + +const ( + AutomationRunStatusPending AutomationRunStatus = "pending" + AutomationRunStatusRunning AutomationRunStatus = "running" + AutomationRunStatusBlocked AutomationRunStatus = "blocked" + AutomationRunStatusCompleted AutomationRunStatus = "completed" + AutomationRunStatusFailed AutomationRunStatus = "failed" + AutomationRunStatusCancelled AutomationRunStatus = "cancelled" +) + +type AutomationRunBlockerKind string + +const ( + AutomationRunBlockerKindUserInput AutomationRunBlockerKind = "userInput" + AutomationRunBlockerKindToolConfirmation AutomationRunBlockerKind = "toolConfirmation" + AutomationRunBlockerKindAuthentication AutomationRunBlockerKind = "authentication" + AutomationRunBlockerKindClientExecution AutomationRunBlockerKind = "clientExecution" +) + +type AutomationRunCauseKind string + +const ( + AutomationRunCauseKindManual AutomationRunCauseKind = "manual" + AutomationRunCauseKindTrigger AutomationRunCauseKind = "trigger" +) + +type AutomationRunOperation string + +const ( + AutomationRunOperationCancel AutomationRunOperation = "cancel" +) + // ─── Structs ────────────────────────────────────────────────────────── // An optionally-sized icon that can be displayed in a user interface. @@ -748,6 +837,8 @@ type SessionState struct { Status SessionStatus `json:"status"` // Human-readable description of what the session is currently doing Activity *string `json:"activity,omitempty"` + // Durable origin of this session, when another AHP resource created it. + Origin *SessionOrigin `json:"origin,omitempty"` // Server-owned project for this session Project *ProjectInfo `json:"project,omitempty"` // The working directories the session's agent has tool access to, as @@ -1022,6 +1113,8 @@ type SessionSummary struct { Status SessionStatus `json:"status"` // Human-readable description of what the session is currently doing Activity *string `json:"activity,omitempty"` + // Durable origin of this session, when another AHP resource created it. + Origin *SessionOrigin `json:"origin,omitempty"` // Server-owned project for this session Project *ProjectInfo `json:"project,omitempty"` // The working directories the session's agent has tool access to, as @@ -3557,6 +3650,228 @@ type ResourceChange struct { Type ResourceChangeType `json:"type"` } +type AutomationSessionOrigin struct { + Kind SessionOriginKind `json:"kind"` + Automation URI `json:"automation"` + Run URI `json:"run"` +} + +type AutomationLocalTime struct { + Hour int64 `json:"hour"` + Minute int64 `json:"minute"` +} + +type AutomationHourlySchedule struct { + Kind AutomationScheduleKind `json:"kind"` +} + +type AutomationDailySchedule struct { + Kind AutomationScheduleKind `json:"kind"` + Time AutomationLocalTime `json:"time"` + // IANA time-zone identifier. + TimeZone string `json:"timeZone"` +} + +type AutomationWeeklySchedule struct { + Kind AutomationScheduleKind `json:"kind"` + Weekday AutomationWeekday `json:"weekday"` + Time AutomationLocalTime `json:"time"` + // IANA time-zone identifier. + TimeZone string `json:"timeZone"` +} + +type AutomationCronSchedule struct { + Kind AutomationScheduleKind `json:"kind"` + // Standard five-field Unix cron expression. + Expression string `json:"expression"` + // IANA time-zone identifier. + TimeZone string `json:"timeZone"` +} + +type AutomationScheduleTrigger struct { + // Stable within the automation definition. + Id string `json:"id"` + Kind AutomationTriggerKind `json:"kind"` + Schedule AutomationSchedule `json:"schedule"` + MisfirePolicy *AutomationMisfirePolicy `json:"misfirePolicy,omitempty"` +} + +type AutomationEventTrigger struct { + // Stable within the automation definition. + Id string `json:"id"` + Kind AutomationTriggerKind `json:"kind"` + // Stable host-defined trigger type. + Type string `json:"type"` + // Selected event actions. + Events []string `json:"events"` + // Schema-defined values. Unknown entries must survive round-trips. + Config map[string]json.RawMessage `json:"config,omitempty"` +} + +type AutomationTriggerEventDefinition struct { + Id string `json:"id"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +type AutomationTriggerDefinition struct { + Type string `json:"type"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` + Events []AutomationTriggerEventDefinition `json:"events"` + ConfigSchema *ConfigSchema `json:"configSchema,omitempty"` +} + +type AutomationSessionTemplate struct { + Provider *string `json:"provider,omitempty"` + // Absence means a workspace-less session. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Values resolved through `resolveSessionConfig`. + Config map[string]json.RawMessage `json:"config,omitempty"` +} + +type AutomationDefinition struct { + Title string `json:"title"` + // Initial user message sent to each new session. + Message Message `json:"message"` + Session AutomationSessionTemplate `json:"session"` + // Controls automatic triggers; manual runs remain permitted. + Enabled bool `json:"enabled"` + // Empty means manual-only. + Triggers []AutomationTrigger `json:"triggers"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + +type AutomationRuntimeState struct { + WorkingDirectories []URI `json:"workingDirectories,omitempty"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + +type AutomationSummary struct { + Resource URI `json:"resource"` + Title string `json:"title"` + Enabled bool `json:"enabled"` + TriggerCount int64 `json:"triggerCount"` + NextRunAt *string `json:"nextRunAt,omitempty"` + LastRun *AutomationRunSummary `json:"lastRun,omitempty"` + Revision int64 `json:"revision"` + Operations []AutomationOperation `json:"operations"` + CreatedAt string `json:"createdAt"` + ModifiedAt string `json:"modifiedAt"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + +type AutomationState struct { + Resource URI `json:"resource"` + Definition AutomationDefinition `json:"definition"` + Revision int64 `json:"revision"` + NextRunAt *string `json:"nextRunAt,omitempty"` + // Newest-first retained run summaries. + Runs []AutomationRunSummary `json:"runs"` + RunsNextCursor *string `json:"runsNextCursor,omitempty"` + Runtime *AutomationRuntimeState `json:"runtime,omitempty"` + Operations []AutomationOperation `json:"operations"` + CreatedAt string `json:"createdAt"` + ModifiedAt string `json:"modifiedAt"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + +type AutomationRunBlocker struct { + Kind AutomationRunBlockerKind `json:"kind"` +} + +type AutomationManualRunCause struct { + Kind AutomationRunCauseKind `json:"kind"` +} + +type AutomationTriggeredRunCause struct { + Kind AutomationRunCauseKind `json:"kind"` + TriggerId string `json:"triggerId"` + ScheduledFor *string `json:"scheduledFor,omitempty"` + CatchUp *bool `json:"catchUp,omitempty"` + // Host-defined event provenance containing no secrets. + Event map[string]json.RawMessage `json:"event,omitempty"` +} + +type AutomationPendingRunLifecycle struct { + Status AutomationRunStatus `json:"status"` + CreatedAt string `json:"createdAt"` +} + +type AutomationRunningRunLifecycle struct { + Status AutomationRunStatus `json:"status"` + CreatedAt string `json:"createdAt"` + StartedAt string `json:"startedAt"` +} + +type AutomationBlockedRunLifecycle struct { + Status AutomationRunStatus `json:"status"` + CreatedAt string `json:"createdAt"` + StartedAt string `json:"startedAt"` + Blocker AutomationRunBlocker `json:"blocker"` +} + +type AutomationCompletedRunLifecycle struct { + Status AutomationRunStatus `json:"status"` + CreatedAt string `json:"createdAt"` + StartedAt string `json:"startedAt"` + CompletedAt string `json:"completedAt"` + Usage *UsageInfo `json:"usage,omitempty"` +} + +type AutomationFailedRunLifecycle struct { + Status AutomationRunStatus `json:"status"` + CreatedAt string `json:"createdAt"` + StartedAt *string `json:"startedAt,omitempty"` + CompletedAt string `json:"completedAt"` + Error ErrorInfo `json:"error"` +} + +type AutomationCancelledRunLifecycle struct { + Status AutomationRunStatus `json:"status"` + CreatedAt string `json:"createdAt"` + StartedAt *string `json:"startedAt,omitempty"` + CompletedAt string `json:"completedAt"` +} + +type AutomationRunArtifact struct { + // Content URI + Uri URI `json:"uri"` + // Approximate size in bytes + SizeHint *int64 `json:"sizeHint,omitempty"` + // Content MIME type + ContentType *string `json:"contentType,omitempty"` + // Content nonce + Nonce *string `json:"nonce,omitempty"` + Id string `json:"id"` + Label string `json:"label"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + +type AutomationRunSummary struct { + Resource URI `json:"resource"` + Automation URI `json:"automation"` + Cause AutomationRunCause `json:"cause"` + Lifecycle AutomationRunLifecycle `json:"lifecycle"` + PrimarySession *URI `json:"primarySession,omitempty"` + SessionCount int64 `json:"sessionCount"` + ArtifactCount *int64 `json:"artifactCount,omitempty"` + Operations []AutomationRunOperation `json:"operations"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + +type AutomationRunState struct { + Resource URI `json:"resource"` + Automation URI `json:"automation"` + Cause AutomationRunCause `json:"cause"` + Lifecycle AutomationRunLifecycle `json:"lifecycle"` + Sessions []URI `json:"sessions"` + PrimarySession *URI `json:"primarySession,omitempty"` + Artifacts []AutomationRunArtifact `json:"artifacts"` + Operations []AutomationRunOperation `json:"operations"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` +} + // ToolInput is raw tool input represented inline or by content reference. type ToolInput struct { Inline *string @@ -4860,6 +5175,361 @@ func (u SessionInputRequest) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// SessionOrigin is the durable origin of a session. +type SessionOrigin struct { + Value isSessionOrigin +} + +// isSessionOrigin is the marker interface implemented by every +// concrete variant of SessionOrigin. +type isSessionOrigin interface{ isSessionOrigin() } + +func (*AutomationSessionOrigin) isSessionOrigin() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *SessionOrigin) UnmarshalJSON(data []byte) error { + disc, ok, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + if !ok { + return missingDiscriminatorError("SessionOrigin", "kind") + } + switch disc { + case "automation": + var value AutomationSessionOrigin + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + return unknownDiscriminatorError("SessionOrigin", "kind", disc) + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u SessionOrigin) MarshalJSON() ([]byte, error) { + if u.Value == nil { + return []byte("null"), nil + } + data, err := json.Marshal(u.Value) + if err != nil { + return nil, err + } + var object map[string]json.RawMessage + if err := json.Unmarshal(data, &object); err != nil { + return nil, err + } + switch u.Value.(type) { + case *AutomationSessionOrigin: + object["kind"] = json.RawMessage("\"automation\"") + } + return json.Marshal(object) +} + +// AutomationSchedule is the calendar schedule for an automation trigger. +type AutomationSchedule struct { + Value isAutomationSchedule +} + +// isAutomationSchedule is the marker interface implemented by every +// concrete variant of AutomationSchedule. +type isAutomationSchedule interface{ isAutomationSchedule() } + +func (*AutomationHourlySchedule) isAutomationSchedule() {} +func (*AutomationDailySchedule) isAutomationSchedule() {} +func (*AutomationWeeklySchedule) isAutomationSchedule() {} +func (*AutomationCronSchedule) isAutomationSchedule() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *AutomationSchedule) UnmarshalJSON(data []byte) error { + disc, ok, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + if !ok { + return missingDiscriminatorError("AutomationSchedule", "kind") + } + switch disc { + case "hourly": + var value AutomationHourlySchedule + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "daily": + var value AutomationDailySchedule + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "weekly": + var value AutomationWeeklySchedule + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "cron": + var value AutomationCronSchedule + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + return unknownDiscriminatorError("AutomationSchedule", "kind", disc) + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u AutomationSchedule) MarshalJSON() ([]byte, error) { + if u.Value == nil { + return []byte("null"), nil + } + data, err := json.Marshal(u.Value) + if err != nil { + return nil, err + } + var object map[string]json.RawMessage + if err := json.Unmarshal(data, &object); err != nil { + return nil, err + } + switch u.Value.(type) { + case *AutomationHourlySchedule: + object["kind"] = json.RawMessage("\"hourly\"") + case *AutomationDailySchedule: + object["kind"] = json.RawMessage("\"daily\"") + case *AutomationWeeklySchedule: + object["kind"] = json.RawMessage("\"weekly\"") + case *AutomationCronSchedule: + object["kind"] = json.RawMessage("\"cron\"") + } + return json.Marshal(object) +} + +// AutomationTrigger is an automatic trigger for an automation. +type AutomationTrigger struct { + Value isAutomationTrigger +} + +// isAutomationTrigger is the marker interface implemented by every +// concrete variant of AutomationTrigger. +type isAutomationTrigger interface{ isAutomationTrigger() } + +func (*AutomationScheduleTrigger) isAutomationTrigger() {} +func (*AutomationEventTrigger) isAutomationTrigger() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *AutomationTrigger) UnmarshalJSON(data []byte) error { + disc, ok, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + if !ok { + return missingDiscriminatorError("AutomationTrigger", "kind") + } + switch disc { + case "schedule": + var value AutomationScheduleTrigger + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "event": + var value AutomationEventTrigger + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + return unknownDiscriminatorError("AutomationTrigger", "kind", disc) + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u AutomationTrigger) MarshalJSON() ([]byte, error) { + if u.Value == nil { + return []byte("null"), nil + } + data, err := json.Marshal(u.Value) + if err != nil { + return nil, err + } + var object map[string]json.RawMessage + if err := json.Unmarshal(data, &object); err != nil { + return nil, err + } + switch u.Value.(type) { + case *AutomationScheduleTrigger: + object["kind"] = json.RawMessage("\"schedule\"") + case *AutomationEventTrigger: + object["kind"] = json.RawMessage("\"event\"") + } + return json.Marshal(object) +} + +// AutomationRunCause is the cause of an automation run. +type AutomationRunCause struct { + Value isAutomationRunCause +} + +// isAutomationRunCause is the marker interface implemented by every +// concrete variant of AutomationRunCause. +type isAutomationRunCause interface{ isAutomationRunCause() } + +func (*AutomationManualRunCause) isAutomationRunCause() {} +func (*AutomationTriggeredRunCause) isAutomationRunCause() {} + +// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. +func (u *AutomationRunCause) UnmarshalJSON(data []byte) error { + disc, ok, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + if !ok { + return missingDiscriminatorError("AutomationRunCause", "kind") + } + switch disc { + case "manual": + var value AutomationManualRunCause + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "trigger": + var value AutomationTriggeredRunCause + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + return unknownDiscriminatorError("AutomationRunCause", "kind", disc) + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u AutomationRunCause) MarshalJSON() ([]byte, error) { + if u.Value == nil { + return []byte("null"), nil + } + data, err := json.Marshal(u.Value) + if err != nil { + return nil, err + } + var object map[string]json.RawMessage + if err := json.Unmarshal(data, &object); err != nil { + return nil, err + } + switch u.Value.(type) { + case *AutomationManualRunCause: + object["kind"] = json.RawMessage("\"manual\"") + case *AutomationTriggeredRunCause: + object["kind"] = json.RawMessage("\"trigger\"") + } + return json.Marshal(object) +} + +// AutomationRunLifecycle is the lifecycle of an automation run. +type AutomationRunLifecycle struct { + Value isAutomationRunLifecycle +} + +// isAutomationRunLifecycle is the marker interface implemented by every +// concrete variant of AutomationRunLifecycle. +type isAutomationRunLifecycle interface{ isAutomationRunLifecycle() } + +func (*AutomationPendingRunLifecycle) isAutomationRunLifecycle() {} +func (*AutomationRunningRunLifecycle) isAutomationRunLifecycle() {} +func (*AutomationBlockedRunLifecycle) isAutomationRunLifecycle() {} +func (*AutomationCompletedRunLifecycle) isAutomationRunLifecycle() {} +func (*AutomationFailedRunLifecycle) isAutomationRunLifecycle() {} +func (*AutomationCancelledRunLifecycle) isAutomationRunLifecycle() {} + +// UnmarshalJSON decodes the variant indicated by the "status" discriminator. +func (u *AutomationRunLifecycle) UnmarshalJSON(data []byte) error { + disc, ok, err := readDiscriminator(data, "status") + if err != nil { + return err + } + if !ok { + return missingDiscriminatorError("AutomationRunLifecycle", "status") + } + switch disc { + case "pending": + var value AutomationPendingRunLifecycle + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "running": + var value AutomationRunningRunLifecycle + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "blocked": + var value AutomationBlockedRunLifecycle + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "completed": + var value AutomationCompletedRunLifecycle + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "failed": + var value AutomationFailedRunLifecycle + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "cancelled": + var value AutomationCancelledRunLifecycle + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + return unknownDiscriminatorError("AutomationRunLifecycle", "status", disc) + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u AutomationRunLifecycle) MarshalJSON() ([]byte, error) { + if u.Value == nil { + return []byte("null"), nil + } + data, err := json.Marshal(u.Value) + if err != nil { + return nil, err + } + var object map[string]json.RawMessage + if err := json.Unmarshal(data, &object); err != nil { + return nil, err + } + switch u.Value.(type) { + case *AutomationPendingRunLifecycle: + object["status"] = json.RawMessage("\"pending\"") + case *AutomationRunningRunLifecycle: + object["status"] = json.RawMessage("\"running\"") + case *AutomationBlockedRunLifecycle: + object["status"] = json.RawMessage("\"blocked\"") + case *AutomationCompletedRunLifecycle: + object["status"] = json.RawMessage("\"completed\"") + case *AutomationFailedRunLifecycle: + object["status"] = json.RawMessage("\"failed\"") + case *AutomationCancelledRunLifecycle: + object["status"] = json.RawMessage("\"cancelled\"") + } + return json.Marshal(object) +} + // ChatOrigin describes how a chat came into existence. type ChatOrigin struct { Value isChatOrigin @@ -4957,10 +5627,12 @@ func (o ChatOrigin) MarshalJSON() ([]byte, error) { } // SnapshotState is the state payload of a snapshot — root, session, -// chat, terminal, changeset, resource-watch, annotations, or content state. The active +// chat, terminal, changeset, resource-watch, annotations, automation, or +// automation-run state. The active // variant is chosen by which pointer field is non-nil; UnmarshalJSON probes // for required fields in the canonical order -// (session → chat → terminal → changeset → resourceWatch → annotations → root). +// (automationRun → automation → session → chat → terminal → changeset → +// resourceWatch → annotations → root). type SnapshotState struct { Root *RootState `json:"-"` Session *SessionState `json:"-"` @@ -4969,11 +5641,17 @@ type SnapshotState struct { Changeset *ChangesetState `json:"-"` ResourceWatch *ResourceWatchState `json:"-"` Annotations *AnnotationsState `json:"-"` + Automation *AutomationState `json:"-"` + AutomationRun *AutomationRunState `json:"-"` } // MarshalJSON encodes whichever variant is currently populated. func (s SnapshotState) MarshalJSON() ([]byte, error) { switch { + case s.AutomationRun != nil: + return json.Marshal(s.AutomationRun) + case s.Automation != nil: + return json.Marshal(s.Automation) case s.Session != nil: return json.Marshal(s.Session) case s.Chat != nil: @@ -5002,6 +5680,18 @@ func (s *SnapshotState) UnmarshalJSON(data []byte) error { return err } switch { + case containsAll(probe, "automation", "cause", "sessions"): + var v AutomationRunState + if err := json.Unmarshal(data, &v); err != nil { + return err + } + s.AutomationRun = &v + case containsAll(probe, "definition"): + var v AutomationState + if err := json.Unmarshal(data, &v); err != nil { + return err + } + s.Automation = &v case containsAll(probe, "lifecycle"): var v SessionState if err := json.Unmarshal(data, &v); err != nil { 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 6222ed855..a1cce0dae 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -1,5 +1,4 @@ -// Reducers.kt — Pure state reducers for AHP root, session, terminal, and -// changeset state. +// Reducers.kt — Pure state reducers for AHP protocol channels. // // Hand-written Kotlin port of the per-channel reducers in // `types/channels-*/reducer.ts`. Behaviour parity with the TypeScript @@ -18,11 +17,13 @@ import kotlinx.serialization.json.JsonElement * A pure state reducer: `reduce(state, action)` returns the next state, with * no mutation of [state] and no side effects. * - * The companion top-level functions ([rootReducer], [sessionReducer], - * [terminalReducer], [changesetReducer], [annotationsReducer], [resourceWatchReducer]) are the canonical implementations. - * The object instances on this interface ([RootReducer], [SessionReducer], - * [TerminalReducer], [ChangesetReducer], [AnnotationsReducer]) wrap them for use as values where - * an instance is needed. + * The companion top-level functions ([rootReducer], [sessionReducer], [chatReducer], + * [terminalReducer], [changesetReducer], [annotationsReducer], [resourceWatchReducer], + * [automationReducer], and [automationRunReducer]) are the canonical implementations. + * The object instances on this interface ([RootReducer], [SessionReducer], [ChatReducer], + * [TerminalReducer], [ChangesetReducer], [AnnotationsReducer], [ResourceWatchReducer], + * [AutomationReducer], and [AutomationRunReducer]) wrap them for use as values where an + * instance is needed. */ public fun interface Reducer { public fun reduce(state: S, action: A): S @@ -70,6 +71,17 @@ public object ResourceWatchReducer : Reducer { resourceWatchReducer(state, action) } +/** Pure automation reducer as a [Reducer] instance. Delegates to [automationReducer]. */ +public object AutomationReducer : Reducer { + override fun reduce(state: AutomationState, action: StateAction): AutomationState = + automationReducer(state, action) +} + +/** Pure automation-run reducer as a [Reducer] instance. Delegates to [automationRunReducer]. */ +public object AutomationRunReducer : Reducer { + override fun reduce(state: AutomationRunState, action: StateAction): AutomationRunState = + automationRunReducer(state, action) +} // ─── Timestamp Provider ───────────────────────────────────────────────────── @@ -1762,3 +1774,106 @@ public fun resourceWatchReducer(state: ResourceWatchState, action: StateAction): is StateActionResourceWatchChanged -> state else -> state } + +// ─── Automation Reducer ───────────────────────────────────────────────────── + +/** Pure reducer for [AutomationState]. Handles automation-channel action variants. */ +public fun automationReducer(state: AutomationState, action: StateAction): AutomationState = when (action) { + is StateActionAutomationDefinitionChanged -> state.copy( + definition = action.value.definition, + revision = action.value.revision, + modifiedAt = action.value.modifiedAt, + nextRunAt = action.value.nextRunAt, + ) + + is StateActionAutomationRunSummarySet -> { + val run = action.value.run + val index = state.runs.indexOfFirst { it.resource == run.resource } + if (index < 0) { + state.copy(runs = listOf(run) + state.runs) + } else { + val runs = state.runs.toMutableList() + runs[index] = run + state.copy(runs = runs) + } + } + + is StateActionAutomationRunSummaryRemoved -> { + val index = state.runs.indexOfFirst { it.resource == action.value.run } + if (index < 0) { + state + } else { + val runs = state.runs.toMutableList() + runs.removeAt(index) + state.copy(runs = runs) + } + } + + is StateActionAutomationRunsLoaded -> { + val known = state.runs.mapTo(mutableSetOf()) { it.resource } + val runs = state.runs + action.value.runs.filter { known.add(it.resource) } + state.copy(runs = runs, runsNextCursor = action.value.nextCursor) + } + + else -> state +} + +// ─── Automation Run Reducer ───────────────────────────────────────────────── + +/** Pure reducer for [AutomationRunState]. Handles automation-run-channel action variants. */ +public fun automationRunReducer(state: AutomationRunState, action: StateAction): AutomationRunState = when (action) { + is StateActionAutomationRunLifecycleChanged -> + state.copy(lifecycle = action.value.lifecycle, operations = action.value.operations) + + is StateActionAutomationRunSessionSet -> + if (action.value.session in state.sessions) { + state + } else { + state.copy(sessions = state.sessions + action.value.session) + } + + is StateActionAutomationRunSessionRemoved -> { + val session = action.value.session + val index = state.sessions.indexOf(session) + if (index < 0) { + state + } else { + val sessions = state.sessions.toMutableList() + sessions.removeAt(index) + state.copy( + sessions = sessions, + primarySession = if (state.primarySession == session) null else state.primarySession, + ) + } + } + + is StateActionAutomationRunPrimarySessionChanged -> + state.copy(primarySession = action.value.primarySession) + + is StateActionAutomationRunArtifactSet -> { + val artifact = action.value.artifact + val index = state.artifacts.indexOfFirst { it.id == artifact.id } + if (index < 0) { + state.copy(artifacts = state.artifacts + artifact) + } else { + val artifacts = state.artifacts.toMutableList() + artifacts[index] = artifact + state.copy(artifacts = artifacts) + } + } + + is StateActionAutomationRunArtifactRemoved -> { + val index = state.artifacts.indexOfFirst { it.id == action.value.artifactId } + if (index < 0) { + state + } else { + val artifacts = state.artifacts.toMutableList() + artifacts.removeAt(index) + state.copy(artifacts = artifacts) + } + } + + is StateActionAutomationRunCancelRequested -> state + + else -> state +} 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 cde9f9ede..aa2b4f7e1 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 @@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.contentOrNull // ─── ActionType ───────────────────────────────────────────────────────────── @@ -195,7 +196,29 @@ enum class ActionType { @SerialName("terminal/commandFinished") TERMINAL_COMMAND_FINISHED, @SerialName("resourceWatch/changed") - RESOURCE_WATCH_CHANGED + RESOURCE_WATCH_CHANGED, + @SerialName("automation/definitionChanged") + AUTOMATION_DEFINITION_CHANGED, + @SerialName("automation/runSummarySet") + AUTOMATION_RUN_SUMMARY_SET, + @SerialName("automation/runSummaryRemoved") + AUTOMATION_RUN_SUMMARY_REMOVED, + @SerialName("automation/runsLoaded") + AUTOMATION_RUNS_LOADED, + @SerialName("automationRun/lifecycleChanged") + AUTOMATION_RUN_LIFECYCLE_CHANGED, + @SerialName("automationRun/sessionSet") + AUTOMATION_RUN_SESSION_SET, + @SerialName("automationRun/sessionRemoved") + AUTOMATION_RUN_SESSION_REMOVED, + @SerialName("automationRun/primarySessionChanged") + AUTOMATION_RUN_PRIMARY_SESSION_CHANGED, + @SerialName("automationRun/artifactSet") + AUTOMATION_RUN_ARTIFACT_SET, + @SerialName("automationRun/artifactRemoved") + AUTOMATION_RUN_ARTIFACT_REMOVED, + @SerialName("automationRun/cancelRequested") + AUTOMATION_RUN_CANCEL_REQUESTED } // ─── Action Infrastructure ────────────────────────────────────────────────── @@ -1480,6 +1503,76 @@ data class ResourceWatchChangedAction( val changes: JsonElement ) +@Serializable +data class AutomationDefinitionChangedAction( + val type: ActionType, + val definition: AutomationDefinition, + val revision: Long, + val modifiedAt: String, + val nextRunAt: String? = null +) + +@Serializable +data class AutomationRunSummarySetAction( + val type: ActionType, + val run: AutomationRunSummary +) + +@Serializable +data class AutomationRunSummaryRemovedAction( + val type: ActionType, + val run: String +) + +@Serializable +data class AutomationRunsLoadedAction( + val type: ActionType, + val runs: List, + val nextCursor: String? = null +) + +@Serializable +data class AutomationRunLifecycleChangedAction( + val type: ActionType, + val lifecycle: AutomationRunLifecycle, + val operations: List +) + +@Serializable +data class AutomationRunSessionSetAction( + val type: ActionType, + val session: String +) + +@Serializable +data class AutomationRunSessionRemovedAction( + val type: ActionType, + val session: String +) + +@Serializable +data class AutomationRunPrimarySessionChangedAction( + val type: ActionType, + val primarySession: String? = null +) + +@Serializable +data class AutomationRunArtifactSetAction( + val type: ActionType, + val artifact: AutomationRunArtifact +) + +@Serializable +data class AutomationRunArtifactRemovedAction( + val type: ActionType, + val artifactId: String +) + +@Serializable +data class AutomationRunCancelRequestedAction( + val type: ActionType +) + // ─── Partial Summary Types ────────────────────────────────────────────────── @Serializable @@ -1622,6 +1715,17 @@ sealed interface StateAction @JvmInline value class StateActionTerminalCommandExecuted(val value: TerminalCommandExecutedAction) : StateAction @JvmInline value class StateActionTerminalCommandFinished(val value: TerminalCommandFinishedAction) : StateAction @JvmInline value class StateActionResourceWatchChanged(val value: ResourceWatchChangedAction) : StateAction +@JvmInline value class StateActionAutomationDefinitionChanged(val value: AutomationDefinitionChangedAction) : StateAction +@JvmInline value class StateActionAutomationRunSummarySet(val value: AutomationRunSummarySetAction) : StateAction +@JvmInline value class StateActionAutomationRunSummaryRemoved(val value: AutomationRunSummaryRemovedAction) : StateAction +@JvmInline value class StateActionAutomationRunsLoaded(val value: AutomationRunsLoadedAction) : StateAction +@JvmInline value class StateActionAutomationRunLifecycleChanged(val value: AutomationRunLifecycleChangedAction) : StateAction +@JvmInline value class StateActionAutomationRunSessionSet(val value: AutomationRunSessionSetAction) : StateAction +@JvmInline value class StateActionAutomationRunSessionRemoved(val value: AutomationRunSessionRemovedAction) : StateAction +@JvmInline value class StateActionAutomationRunPrimarySessionChanged(val value: AutomationRunPrimarySessionChangedAction) : StateAction +@JvmInline value class StateActionAutomationRunArtifactSet(val value: AutomationRunArtifactSetAction) : StateAction +@JvmInline value class StateActionAutomationRunArtifactRemoved(val value: AutomationRunArtifactRemovedAction) : StateAction +@JvmInline value class StateActionAutomationRunCancelRequested(val value: AutomationRunCancelRequestedAction) : StateAction @JvmInline value class StateActionUnknown(val raw: JsonObject) : StateAction internal object StateActionSerializer : KSerializer { @@ -1722,6 +1826,17 @@ internal object StateActionSerializer : KSerializer { "terminal/commandExecuted" -> StateActionTerminalCommandExecuted(input.json.decodeFromJsonElement(TerminalCommandExecutedAction.serializer(), element)) "terminal/commandFinished" -> StateActionTerminalCommandFinished(input.json.decodeFromJsonElement(TerminalCommandFinishedAction.serializer(), element)) "resourceWatch/changed" -> StateActionResourceWatchChanged(input.json.decodeFromJsonElement(ResourceWatchChangedAction.serializer(), element)) + "automation/definitionChanged" -> StateActionAutomationDefinitionChanged(input.json.decodeFromJsonElement(AutomationDefinitionChangedAction.serializer(), element)) + "automation/runSummarySet" -> StateActionAutomationRunSummarySet(input.json.decodeFromJsonElement(AutomationRunSummarySetAction.serializer(), element)) + "automation/runSummaryRemoved" -> StateActionAutomationRunSummaryRemoved(input.json.decodeFromJsonElement(AutomationRunSummaryRemovedAction.serializer(), element)) + "automation/runsLoaded" -> StateActionAutomationRunsLoaded(input.json.decodeFromJsonElement(AutomationRunsLoadedAction.serializer(), element)) + "automationRun/lifecycleChanged" -> StateActionAutomationRunLifecycleChanged(input.json.decodeFromJsonElement(AutomationRunLifecycleChangedAction.serializer(), element)) + "automationRun/sessionSet" -> StateActionAutomationRunSessionSet(input.json.decodeFromJsonElement(AutomationRunSessionSetAction.serializer(), element)) + "automationRun/sessionRemoved" -> StateActionAutomationRunSessionRemoved(input.json.decodeFromJsonElement(AutomationRunSessionRemovedAction.serializer(), element)) + "automationRun/primarySessionChanged" -> StateActionAutomationRunPrimarySessionChanged(input.json.decodeFromJsonElement(AutomationRunPrimarySessionChangedAction.serializer(), element)) + "automationRun/artifactSet" -> StateActionAutomationRunArtifactSet(input.json.decodeFromJsonElement(AutomationRunArtifactSetAction.serializer(), element)) + "automationRun/artifactRemoved" -> StateActionAutomationRunArtifactRemoved(input.json.decodeFromJsonElement(AutomationRunArtifactRemovedAction.serializer(), element)) + "automationRun/cancelRequested" -> StateActionAutomationRunCancelRequested(input.json.decodeFromJsonElement(AutomationRunCancelRequestedAction.serializer(), element)) else -> StateActionUnknown(obj) } } @@ -1815,6 +1930,17 @@ internal object StateActionSerializer : KSerializer { is StateActionTerminalCommandExecuted -> output.json.encodeToJsonElement(TerminalCommandExecutedAction.serializer(), value.value) is StateActionTerminalCommandFinished -> output.json.encodeToJsonElement(TerminalCommandFinishedAction.serializer(), value.value) is StateActionResourceWatchChanged -> output.json.encodeToJsonElement(ResourceWatchChangedAction.serializer(), value.value) + is StateActionAutomationDefinitionChanged -> output.json.encodeToJsonElement(AutomationDefinitionChangedAction.serializer(), value.value) + is StateActionAutomationRunSummarySet -> output.json.encodeToJsonElement(AutomationRunSummarySetAction.serializer(), value.value) + is StateActionAutomationRunSummaryRemoved -> output.json.encodeToJsonElement(AutomationRunSummaryRemovedAction.serializer(), value.value) + is StateActionAutomationRunsLoaded -> output.json.encodeToJsonElement(AutomationRunsLoadedAction.serializer(), value.value) + is StateActionAutomationRunLifecycleChanged -> output.json.encodeToJsonElement(AutomationRunLifecycleChangedAction.serializer(), value.value) + is StateActionAutomationRunSessionSet -> output.json.encodeToJsonElement(AutomationRunSessionSetAction.serializer(), value.value) + is StateActionAutomationRunSessionRemoved -> output.json.encodeToJsonElement(AutomationRunSessionRemovedAction.serializer(), value.value) + is StateActionAutomationRunPrimarySessionChanged -> output.json.encodeToJsonElement(AutomationRunPrimarySessionChangedAction.serializer(), value.value) + is StateActionAutomationRunArtifactSet -> output.json.encodeToJsonElement(AutomationRunArtifactSetAction.serializer(), value.value) + is StateActionAutomationRunArtifactRemoved -> output.json.encodeToJsonElement(AutomationRunArtifactRemovedAction.serializer(), value.value) + is StateActionAutomationRunCancelRequested -> output.json.encodeToJsonElement(AutomationRunCancelRequestedAction.serializer(), value.value) is StateActionUnknown -> value.raw } output.encodeJsonElement(element) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 1f07e8c1c..62fca22d9 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.contentOrNull // ─── Command Enums ────────────────────────────────────────────────────────── @@ -339,7 +340,11 @@ data class InitializeResult( * defines a template variable, `{level}`, for subscriber-side severity * filtering). Clients MAY ignore signals they cannot process. */ - val telemetry: TelemetryCapabilities? = null + val telemetry: TelemetryCapabilities? = null, + /** + * Host automation support. Absence means unsupported. + */ + val automations: AutomationCapabilities? = null ) @Serializable @@ -360,6 +365,42 @@ data class ClientCapabilities( val mcpApps: Map? = null ) +@Serializable +data class AutomationCapabilities( + val execution: AutomationExecutionCapabilities, + val create: AutomationCreateCapability? = null, + val schedules: AutomationScheduleCapabilities? = null, + val runCancellation: AutomationRunCancellationCapability? = null, + val schedulePreview: AutomationSchedulePreviewCapability? = null, + val runHistoryLimit: Long? = null +) + +@Serializable +data class AutomationExecutionCapabilities( + val lifetime: AutomationExecutionLifetime +) + +@Serializable +class AutomationCreateCapability + +@Serializable +data class AutomationScheduleCapabilities( + val kinds: List, + val cron: AutomationCronScheduleCapability? = null +) + +@Serializable +data class AutomationCronScheduleCapability( + val dialect: String, + val minIntervalMinutes: Long? = null +) + +@Serializable +class AutomationRunCancellationCapability + +@Serializable +class AutomationSchedulePreviewCapability + @Serializable data class Implementation( /** @@ -1497,6 +1538,136 @@ data class ChangesetOperationFollowUp( val external: Boolean? = null ) +@Serializable +data class ListAutomationsParams( + /** + * Channel URI this command targets. + */ + val channel: String, + /** + * Maximum number of entries to return in this page. The server SHOULD respect + * this bound but MAY return fewer entries and MAY impose its own upper cap. + * Omit to let the server choose the page size. + */ + val limit: Long? = null, + /** + * Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + * Omit to fetch the first page. Cursors are server-defined and MUST be treated + * as opaque — do not parse, modify, or persist them across connections. An + * unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + */ + val cursor: String? = null, + val enabled: Boolean? = null +) + +@Serializable +data class ListAutomationsResult( + /** + * Opaque cursor for the next page. Present when more entries exist beyond the + * returned page; absent signals the end of the collection. Pass it back as + * {@link PaginatedParams.cursor} to fetch the following page. + */ + val nextCursor: String? = null, + val items: List +) + +@Serializable +data class ListAutomationTriggerDefinitionsParams( + /** + * Channel URI this command targets. + */ + val channel: String, + val provider: String? = null, + val workingDirectories: List? = null, + val sessionConfig: Map? = null +) + +@Serializable +data class ListAutomationTriggerDefinitionsResult( + val items: List +) + +@Serializable +data class CreateAutomationParams( + /** + * Channel URI this command targets. + */ + val channel: String, + val definition: AutomationDefinition, + @SerialName("import") + val `import`: JsonElement? = null +) + +@Serializable +data class AutomationDefinitionPatch( + val title: String? = null, + val message: Message? = null, + val session: AutomationSessionTemplate? = null, + val enabled: Boolean? = null, + val triggers: List? = null, + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class UpdateAutomationParams( + /** + * Channel URI this command targets. + */ + val channel: String, + val expectedRevision: Long, + val changes: AutomationDefinitionPatch +) + +@Serializable +data class DisposeAutomationParams( + /** + * Channel URI this command targets. + */ + val channel: String +) + +@Serializable +data class RunAutomationParams( + /** + * Channel URI this command targets. + */ + val channel: String, + val requestId: String +) + +@Serializable +data class RunAutomationResult( + val run: String +) + +@Serializable +data class FetchAutomationRunsParams( + /** + * Channel URI this command targets. + */ + val channel: String, + val cursor: String? = null +) + +@Serializable +class FetchAutomationRunsResult + +@Serializable +data class PreviewAutomationScheduleParams( + /** + * Channel URI this command targets. + */ + val channel: String, + val schedule: AutomationSchedule, + val count: Long? = null +) + +@Serializable +data class PreviewAutomationScheduleResult( + val items: List +) + // ─── ChatSource Union ─────────────────────────────────────────────────────── @Serializable(with = ChatSourceSerializer::class) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt index 9aeea82f8..c97ee400c 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Errors.generated.kt @@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.contentOrNull // ─── Standard JSON-RPC Error Codes ────────────────────────────────────────── diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt index a87cd545a..3209e3e15 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Messages.generated.kt @@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.contentOrNull // ─── JSON-RPC Base Types ──────────────────────────────────────────────────── diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 64e93a151..4cc3de4a3 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.contentOrNull // ─── Notification Enums ───────────────────────────────────────────────────── @@ -83,6 +84,24 @@ data class SessionSummaryChangedParams( val changes: PartialSessionSummary ) +@Serializable +data class AutomationAddedParams( + val channel: String, + val summary: AutomationSummary +) + +@Serializable +data class AutomationRemovedParams( + val channel: String, + val automation: String +) + +@Serializable +data class AutomationSummaryChangedParams( + val channel: String, + val summary: AutomationSummary +) + @Serializable data class ProgressParams( /** @@ -191,6 +210,10 @@ data class PartialSessionSummary( * Human-readable description of what the session is currently doing */ val activity: String? = null, + /** + * Durable origin of this session, when another AHP resource created it. + */ + val origin: SessionOrigin? = null, /** * Server-owned project for this session */ 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 d6fe2b4e6..98f4682c3 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 @@ -17,6 +17,7 @@ import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.contentOrNull // ─── Type Aliases ─────────────────────────────────────────────────────────── @@ -750,6 +751,118 @@ enum class ResourceChangeType { DELETED } +@Serializable +enum class SessionOriginKind { + @SerialName("automation") + AUTOMATION +} + +@Serializable +enum class AutomationOperation { + @SerialName("update") + UPDATE, + @SerialName("dispose") + DISPOSE, + @SerialName("run") + RUN +} + +@Serializable +enum class AutomationExecutionLifetime { + @SerialName("hostLifetime") + HOST_LIFETIME, + @SerialName("managed") + MANAGED +} + +@Serializable +enum class AutomationScheduleKind { + @SerialName("hourly") + HOURLY, + @SerialName("daily") + DAILY, + @SerialName("weekly") + WEEKLY, + @SerialName("cron") + CRON +} + +@Serializable +enum class AutomationWeekday { + @SerialName("monday") + MONDAY, + @SerialName("tuesday") + TUESDAY, + @SerialName("wednesday") + WEDNESDAY, + @SerialName("thursday") + THURSDAY, + @SerialName("friday") + FRIDAY, + @SerialName("saturday") + SATURDAY, + @SerialName("sunday") + SUNDAY +} + +@Serializable +enum class AutomationMisfirePolicy { + @SerialName("skip") + SKIP, + @SerialName("runOnce") + RUN_ONCE +} + +@Serializable +enum class AutomationTriggerKind { + @SerialName("schedule") + SCHEDULE, + @SerialName("event") + EVENT +} + +@Serializable +enum class AutomationRunStatus { + @SerialName("pending") + PENDING, + @SerialName("running") + RUNNING, + @SerialName("blocked") + BLOCKED, + @SerialName("completed") + COMPLETED, + @SerialName("failed") + FAILED, + @SerialName("cancelled") + CANCELLED +} + +@Serializable +enum class AutomationRunBlockerKind { + @SerialName("userInput") + USER_INPUT, + @SerialName("toolConfirmation") + TOOL_CONFIRMATION, + @SerialName("authentication") + AUTHENTICATION, + @SerialName("clientExecution") + CLIENT_EXECUTION +} + +@Serializable +enum class AutomationRunCauseKind { + @SerialName("manual") + MANUAL, + @SerialName("trigger") + TRIGGER +} + +@Serializable +enum class AutomationRunOperation { + @SerialName("cancel") + CANCEL +} + // ─── State Types ──────────────────────────────────────────────────────────── @Serializable @@ -1329,6 +1442,10 @@ data class SessionState( * Human-readable description of what the session is currently doing */ val activity: String? = null, + /** + * Durable origin of this session, when another AHP resource created it. + */ + val origin: SessionOrigin? = null, /** * Server-owned project for this session */ @@ -1606,6 +1723,10 @@ data class SessionSummary( * Human-readable description of what the session is currently doing */ val activity: String? = null, + /** + * Durable origin of this session, when another AHP resource created it. + */ + val origin: SessionOrigin? = null, /** * Server-owned project for this session */ @@ -4662,6 +4783,302 @@ data class ResourceChange( val type: ResourceChangeType ) +@Serializable +data class AutomationSessionOrigin( + val kind: SessionOriginKind, + val automation: String, + val run: String +) + +@Serializable +data class AutomationLocalTime( + val hour: Long, + val minute: Long +) + +@Serializable +data class AutomationHourlySchedule( + val kind: AutomationScheduleKind +) + +@Serializable +data class AutomationDailySchedule( + val kind: AutomationScheduleKind, + val time: AutomationLocalTime, + /** + * IANA time-zone identifier. + */ + val timeZone: String +) + +@Serializable +data class AutomationWeeklySchedule( + val kind: AutomationScheduleKind, + val weekday: AutomationWeekday, + val time: AutomationLocalTime, + /** + * IANA time-zone identifier. + */ + val timeZone: String +) + +@Serializable +data class AutomationCronSchedule( + val kind: AutomationScheduleKind, + /** + * Standard five-field Unix cron expression. + */ + val expression: String, + /** + * IANA time-zone identifier. + */ + val timeZone: String +) + +@Serializable +data class AutomationScheduleTrigger( + /** + * Stable within the automation definition. + */ + val id: String, + val kind: AutomationTriggerKind, + val schedule: AutomationSchedule, + val misfirePolicy: AutomationMisfirePolicy? = null +) + +@Serializable +data class AutomationEventTrigger( + /** + * Stable within the automation definition. + */ + val id: String, + val kind: AutomationTriggerKind, + /** + * Stable host-defined trigger type. + */ + val type: String, + /** + * Selected event actions. + */ + val events: List, + /** + * Schema-defined values. Unknown entries must survive round-trips. + */ + val config: Map? = null +) + +@Serializable +data class AutomationTriggerEventDefinition( + val id: String, + val title: String, + val description: String? = null +) + +@Serializable +data class AutomationTriggerDefinition( + val type: String, + val title: String, + val description: String? = null, + val events: List, + val configSchema: ConfigSchema? = null +) + +@Serializable +data class AutomationSessionTemplate( + val provider: String? = null, + /** + * Absence means a workspace-less session. + */ + val workingDirectories: List? = null, + /** + * Values resolved through `resolveSessionConfig`. + */ + val config: Map? = null +) + +@Serializable +data class AutomationDefinition( + val title: String, + /** + * Initial user message sent to each new session. + */ + val message: Message, + val session: AutomationSessionTemplate, + /** + * Controls automatic triggers; manual runs remain permitted. + */ + val enabled: Boolean, + /** + * Empty means manual-only. + */ + val triggers: List, + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class AutomationRuntimeState( + val workingDirectories: List? = null, + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class AutomationSummary( + val resource: String, + val title: String, + val enabled: Boolean, + val triggerCount: Long, + val nextRunAt: String? = null, + val lastRun: AutomationRunSummary? = null, + val revision: Long, + val operations: List, + val createdAt: String, + val modifiedAt: String, + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class AutomationState( + val resource: String, + val definition: AutomationDefinition, + val revision: Long, + val nextRunAt: String? = null, + /** + * Newest-first retained run summaries. + */ + val runs: List, + val runsNextCursor: String? = null, + val runtime: AutomationRuntimeState? = null, + val operations: List, + val createdAt: String, + val modifiedAt: String, + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class AutomationRunBlocker( + val kind: AutomationRunBlockerKind +) + +@Serializable +data class AutomationManualRunCause( + val kind: AutomationRunCauseKind +) + +@Serializable +data class AutomationTriggeredRunCause( + val kind: AutomationRunCauseKind, + val triggerId: String, + val scheduledFor: String? = null, + val catchUp: Boolean? = null, + /** + * Host-defined event provenance containing no secrets. + */ + val event: Map? = null +) + +@Serializable +data class AutomationPendingRunLifecycle( + val status: AutomationRunStatus, + val createdAt: String +) + +@Serializable +data class AutomationRunningRunLifecycle( + val status: AutomationRunStatus, + val createdAt: String, + val startedAt: String +) + +@Serializable +data class AutomationBlockedRunLifecycle( + val status: AutomationRunStatus, + val createdAt: String, + val startedAt: String, + val blocker: AutomationRunBlocker +) + +@Serializable +data class AutomationCompletedRunLifecycle( + val status: AutomationRunStatus, + val createdAt: String, + val startedAt: String, + val completedAt: String, + val usage: UsageInfo? = null +) + +@Serializable +data class AutomationFailedRunLifecycle( + val status: AutomationRunStatus, + val createdAt: String, + val startedAt: String? = null, + val completedAt: String, + val error: ErrorInfo +) + +@Serializable +data class AutomationCancelledRunLifecycle( + val status: AutomationRunStatus, + val createdAt: String, + val startedAt: String? = null, + val completedAt: String +) + +@Serializable +data class AutomationRunArtifact( + /** + * Content URI + */ + val uri: String, + /** + * Approximate size in bytes + */ + val sizeHint: Long? = null, + /** + * Content MIME type + */ + val contentType: String? = null, + /** + * Content nonce + */ + val nonce: String? = null, + val id: String, + val label: String, + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class AutomationRunSummary( + val resource: String, + val automation: String, + val cause: AutomationRunCause, + val lifecycle: AutomationRunLifecycle, + val primarySession: String? = null, + val sessionCount: Long, + val artifactCount: Long? = null, + val operations: List, + @SerialName("_meta") + val meta: Map? = null +) + +@Serializable +data class AutomationRunState( + val resource: String, + val automation: String, + val cause: AutomationRunCause, + val lifecycle: AutomationRunLifecycle, + val sessions: List, + val primarySession: String? = null, + val artifacts: List, + val operations: List, + @SerialName("_meta") + val meta: Map? = null +) + // ─── Tool Input ────────────────────────────────────────────────────────────── /** @@ -5676,6 +6093,251 @@ internal object SessionInputRequestSerializer : KSerializer } } +@Serializable(with = SessionOriginSerializer::class) +sealed interface SessionOrigin + +@JvmInline +value class SessionOriginAutomation(val value: AutomationSessionOrigin) : SessionOrigin + +internal object SessionOriginSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("SessionOrigin") + + override fun deserialize(decoder: Decoder): SessionOrigin { + val input = decoder as? JsonDecoder + ?: error("SessionOrigin can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for SessionOrigin") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: error("Missing kind discriminator on SessionOrigin") + return when (discriminant) { + "automation" -> SessionOriginAutomation(input.json.decodeFromJsonElement(AutomationSessionOrigin.serializer(), element)) + else -> error("Unknown SessionOrigin discriminator: $discriminant") + } + } + + override fun serialize(encoder: Encoder, value: SessionOrigin) { + val output = encoder as? JsonEncoder + ?: error("SessionOrigin can only be serialized to JSON") + val element: JsonElement = when (value) { + is SessionOriginAutomation -> output.json.encodeToJsonElement(AutomationSessionOrigin.serializer(), value.value) + } + val encodedObject = element.jsonObject.toMutableMap() + val discriminant = when (value) { + is SessionOriginAutomation -> "automation" + } + if (discriminant != null) encodedObject["kind"] = JsonPrimitive(discriminant) + output.encodeJsonElement(JsonObject(encodedObject)) + } +} + +@Serializable(with = AutomationScheduleSerializer::class) +sealed interface AutomationSchedule + +@JvmInline +value class AutomationScheduleHourly(val value: AutomationHourlySchedule) : AutomationSchedule +@JvmInline +value class AutomationScheduleDaily(val value: AutomationDailySchedule) : AutomationSchedule +@JvmInline +value class AutomationScheduleWeekly(val value: AutomationWeeklySchedule) : AutomationSchedule +@JvmInline +value class AutomationScheduleCron(val value: AutomationCronSchedule) : AutomationSchedule + +internal object AutomationScheduleSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("AutomationSchedule") + + override fun deserialize(decoder: Decoder): AutomationSchedule { + val input = decoder as? JsonDecoder + ?: error("AutomationSchedule can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for AutomationSchedule") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: error("Missing kind discriminator on AutomationSchedule") + return when (discriminant) { + "hourly" -> AutomationScheduleHourly(input.json.decodeFromJsonElement(AutomationHourlySchedule.serializer(), element)) + "daily" -> AutomationScheduleDaily(input.json.decodeFromJsonElement(AutomationDailySchedule.serializer(), element)) + "weekly" -> AutomationScheduleWeekly(input.json.decodeFromJsonElement(AutomationWeeklySchedule.serializer(), element)) + "cron" -> AutomationScheduleCron(input.json.decodeFromJsonElement(AutomationCronSchedule.serializer(), element)) + else -> error("Unknown AutomationSchedule discriminator: $discriminant") + } + } + + override fun serialize(encoder: Encoder, value: AutomationSchedule) { + val output = encoder as? JsonEncoder + ?: error("AutomationSchedule can only be serialized to JSON") + val element: JsonElement = when (value) { + is AutomationScheduleHourly -> output.json.encodeToJsonElement(AutomationHourlySchedule.serializer(), value.value) + is AutomationScheduleDaily -> output.json.encodeToJsonElement(AutomationDailySchedule.serializer(), value.value) + is AutomationScheduleWeekly -> output.json.encodeToJsonElement(AutomationWeeklySchedule.serializer(), value.value) + is AutomationScheduleCron -> output.json.encodeToJsonElement(AutomationCronSchedule.serializer(), value.value) + } + val encodedObject = element.jsonObject.toMutableMap() + val discriminant = when (value) { + is AutomationScheduleHourly -> "hourly" + is AutomationScheduleDaily -> "daily" + is AutomationScheduleWeekly -> "weekly" + is AutomationScheduleCron -> "cron" + } + if (discriminant != null) encodedObject["kind"] = JsonPrimitive(discriminant) + output.encodeJsonElement(JsonObject(encodedObject)) + } +} + +@Serializable(with = AutomationTriggerSerializer::class) +sealed interface AutomationTrigger + +@JvmInline +value class AutomationTriggerSchedule(val value: AutomationScheduleTrigger) : AutomationTrigger +@JvmInline +value class AutomationTriggerEvent(val value: AutomationEventTrigger) : AutomationTrigger + +internal object AutomationTriggerSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("AutomationTrigger") + + override fun deserialize(decoder: Decoder): AutomationTrigger { + val input = decoder as? JsonDecoder + ?: error("AutomationTrigger can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for AutomationTrigger") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: error("Missing kind discriminator on AutomationTrigger") + return when (discriminant) { + "schedule" -> AutomationTriggerSchedule(input.json.decodeFromJsonElement(AutomationScheduleTrigger.serializer(), element)) + "event" -> AutomationTriggerEvent(input.json.decodeFromJsonElement(AutomationEventTrigger.serializer(), element)) + else -> error("Unknown AutomationTrigger discriminator: $discriminant") + } + } + + override fun serialize(encoder: Encoder, value: AutomationTrigger) { + val output = encoder as? JsonEncoder + ?: error("AutomationTrigger can only be serialized to JSON") + val element: JsonElement = when (value) { + is AutomationTriggerSchedule -> output.json.encodeToJsonElement(AutomationScheduleTrigger.serializer(), value.value) + is AutomationTriggerEvent -> output.json.encodeToJsonElement(AutomationEventTrigger.serializer(), value.value) + } + val encodedObject = element.jsonObject.toMutableMap() + val discriminant = when (value) { + is AutomationTriggerSchedule -> "schedule" + is AutomationTriggerEvent -> "event" + } + if (discriminant != null) encodedObject["kind"] = JsonPrimitive(discriminant) + output.encodeJsonElement(JsonObject(encodedObject)) + } +} + +@Serializable(with = AutomationRunCauseSerializer::class) +sealed interface AutomationRunCause + +@JvmInline +value class AutomationRunCauseManual(val value: AutomationManualRunCause) : AutomationRunCause +@JvmInline +value class AutomationRunCauseTrigger(val value: AutomationTriggeredRunCause) : AutomationRunCause + +internal object AutomationRunCauseSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("AutomationRunCause") + + override fun deserialize(decoder: Decoder): AutomationRunCause { + val input = decoder as? JsonDecoder + ?: error("AutomationRunCause can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for AutomationRunCause") + val discriminant = (obj["kind"] as? JsonPrimitive)?.content + ?: error("Missing kind discriminator on AutomationRunCause") + return when (discriminant) { + "manual" -> AutomationRunCauseManual(input.json.decodeFromJsonElement(AutomationManualRunCause.serializer(), element)) + "trigger" -> AutomationRunCauseTrigger(input.json.decodeFromJsonElement(AutomationTriggeredRunCause.serializer(), element)) + else -> error("Unknown AutomationRunCause discriminator: $discriminant") + } + } + + override fun serialize(encoder: Encoder, value: AutomationRunCause) { + val output = encoder as? JsonEncoder + ?: error("AutomationRunCause can only be serialized to JSON") + val element: JsonElement = when (value) { + is AutomationRunCauseManual -> output.json.encodeToJsonElement(AutomationManualRunCause.serializer(), value.value) + is AutomationRunCauseTrigger -> output.json.encodeToJsonElement(AutomationTriggeredRunCause.serializer(), value.value) + } + val encodedObject = element.jsonObject.toMutableMap() + val discriminant = when (value) { + is AutomationRunCauseManual -> "manual" + is AutomationRunCauseTrigger -> "trigger" + } + if (discriminant != null) encodedObject["kind"] = JsonPrimitive(discriminant) + output.encodeJsonElement(JsonObject(encodedObject)) + } +} + +@Serializable(with = AutomationRunLifecycleSerializer::class) +sealed interface AutomationRunLifecycle + +@JvmInline +value class AutomationRunLifecyclePending(val value: AutomationPendingRunLifecycle) : AutomationRunLifecycle +@JvmInline +value class AutomationRunLifecycleRunning(val value: AutomationRunningRunLifecycle) : AutomationRunLifecycle +@JvmInline +value class AutomationRunLifecycleBlocked(val value: AutomationBlockedRunLifecycle) : AutomationRunLifecycle +@JvmInline +value class AutomationRunLifecycleCompleted(val value: AutomationCompletedRunLifecycle) : AutomationRunLifecycle +@JvmInline +value class AutomationRunLifecycleFailed(val value: AutomationFailedRunLifecycle) : AutomationRunLifecycle +@JvmInline +value class AutomationRunLifecycleCancelled(val value: AutomationCancelledRunLifecycle) : AutomationRunLifecycle + +internal object AutomationRunLifecycleSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("AutomationRunLifecycle") + + override fun deserialize(decoder: Decoder): AutomationRunLifecycle { + val input = decoder as? JsonDecoder + ?: error("AutomationRunLifecycle can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for AutomationRunLifecycle") + val discriminant = (obj["status"] as? JsonPrimitive)?.content + ?: error("Missing status discriminator on AutomationRunLifecycle") + return when (discriminant) { + "pending" -> AutomationRunLifecyclePending(input.json.decodeFromJsonElement(AutomationPendingRunLifecycle.serializer(), element)) + "running" -> AutomationRunLifecycleRunning(input.json.decodeFromJsonElement(AutomationRunningRunLifecycle.serializer(), element)) + "blocked" -> AutomationRunLifecycleBlocked(input.json.decodeFromJsonElement(AutomationBlockedRunLifecycle.serializer(), element)) + "completed" -> AutomationRunLifecycleCompleted(input.json.decodeFromJsonElement(AutomationCompletedRunLifecycle.serializer(), element)) + "failed" -> AutomationRunLifecycleFailed(input.json.decodeFromJsonElement(AutomationFailedRunLifecycle.serializer(), element)) + "cancelled" -> AutomationRunLifecycleCancelled(input.json.decodeFromJsonElement(AutomationCancelledRunLifecycle.serializer(), element)) + else -> error("Unknown AutomationRunLifecycle discriminator: $discriminant") + } + } + + override fun serialize(encoder: Encoder, value: AutomationRunLifecycle) { + val output = encoder as? JsonEncoder + ?: error("AutomationRunLifecycle can only be serialized to JSON") + val element: JsonElement = when (value) { + is AutomationRunLifecyclePending -> output.json.encodeToJsonElement(AutomationPendingRunLifecycle.serializer(), value.value) + is AutomationRunLifecycleRunning -> output.json.encodeToJsonElement(AutomationRunningRunLifecycle.serializer(), value.value) + is AutomationRunLifecycleBlocked -> output.json.encodeToJsonElement(AutomationBlockedRunLifecycle.serializer(), value.value) + is AutomationRunLifecycleCompleted -> output.json.encodeToJsonElement(AutomationCompletedRunLifecycle.serializer(), value.value) + is AutomationRunLifecycleFailed -> output.json.encodeToJsonElement(AutomationFailedRunLifecycle.serializer(), value.value) + is AutomationRunLifecycleCancelled -> output.json.encodeToJsonElement(AutomationCancelledRunLifecycle.serializer(), value.value) + } + val encodedObject = element.jsonObject.toMutableMap() + val discriminant = when (value) { + is AutomationRunLifecyclePending -> "pending" + is AutomationRunLifecycleRunning -> "running" + is AutomationRunLifecycleBlocked -> "blocked" + is AutomationRunLifecycleCompleted -> "completed" + is AutomationRunLifecycleFailed -> "failed" + is AutomationRunLifecycleCancelled -> "cancelled" + } + if (discriminant != null) encodedObject["status"] = JsonPrimitive(discriminant) + output.encodeJsonElement(JsonObject(encodedObject)) + } +} + @Serializable(with = ToolResultContentSerializer::class) sealed interface ToolResultContent { @JvmInline value class Text(val value: ToolResultTextContent) : ToolResultContent @@ -5734,8 +6396,7 @@ internal object ToolResultContentSerializer : KSerializer { } /** - * The state payload of a snapshot — root, session, chat, terminal, changeset, - * resource-watch, annotations, or content state. + * The state payload of a snapshot. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { @@ -5746,6 +6407,8 @@ sealed interface SnapshotState { @JvmInline value class Changeset(val value: ChangesetState) : SnapshotState @JvmInline value class ResourceWatch(val value: ResourceWatchState) : SnapshotState @JvmInline value class Annotations(val value: AnnotationsState) : SnapshotState + @JvmInline value class Automation(val value: AutomationState) : SnapshotState + @JvmInline value class AutomationRun(val value: AutomationRunState) : SnapshotState } internal object SnapshotStateSerializer : KSerializer { @@ -5758,7 +6421,9 @@ internal object SnapshotStateSerializer : KSerializer { val element = input.decodeJsonElement() val obj = element as? JsonObject ?: error("Expected JsonObject for SnapshotState") - // Try the most distinctive shape first. SessionState has required + // Try the most distinctive shape first. AutomationRunState has required + // `automation`, `cause`, and `sessions`; AutomationState has required + // `definition`; SessionState has required // `lifecycle`; ChatState has required `turns`; ChangesetState has // required `status` + `files`; ResourceWatchState has required // `root` + `recursive`; AnnotationsState has required `annotations` @@ -5766,6 +6431,10 @@ internal object SnapshotStateSerializer : KSerializer { // key); TerminalState has required `content`; RootState is the // catch-all. return when { + obj.containsKey("automation") && obj.containsKey("cause") && obj.containsKey("sessions") -> + SnapshotState.AutomationRun(input.json.decodeFromJsonElement(AutomationRunState.serializer(), element)) + obj.containsKey("definition") -> + SnapshotState.Automation(input.json.decodeFromJsonElement(AutomationState.serializer(), element)) obj.containsKey("lifecycle") -> SnapshotState.Session(input.json.decodeFromJsonElement(SessionState.serializer(), element)) obj.containsKey("turns") -> SnapshotState.Chat(input.json.decodeFromJsonElement(ChatState.serializer(), element)) obj.containsKey("status") && obj.containsKey("files") -> @@ -5791,6 +6460,8 @@ internal object SnapshotStateSerializer : KSerializer { is SnapshotState.Changeset -> output.json.encodeToJsonElement(ChangesetState.serializer(), value.value) is SnapshotState.ResourceWatch -> output.json.encodeToJsonElement(ResourceWatchState.serializer(), value.value) is SnapshotState.Annotations -> output.json.encodeToJsonElement(AnnotationsState.serializer(), value.value) + is SnapshotState.Automation -> output.json.encodeToJsonElement(AutomationState.serializer(), value.value) + is SnapshotState.AutomationRun -> output.json.encodeToJsonElement(AutomationRunState.serializer(), value.value) } output.encodeJsonElement(element) } diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt index fbe2e4c48..432fe6ae5 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt @@ -1,5 +1,10 @@ package com.microsoft.agenthostprotocol +import com.microsoft.agenthostprotocol.generated.AutomationDailySchedule +import com.microsoft.agenthostprotocol.generated.AutomationLocalTime +import com.microsoft.agenthostprotocol.generated.AutomationSchedule +import com.microsoft.agenthostprotocol.generated.AutomationScheduleDaily +import com.microsoft.agenthostprotocol.generated.AutomationScheduleKind import com.microsoft.agenthostprotocol.generated.ChangesetOperationRangeTarget import com.microsoft.agenthostprotocol.generated.ChangesetOperationResourceTarget import com.microsoft.agenthostprotocol.generated.ChangesetOperationTarget @@ -82,6 +87,20 @@ class DiscriminatedUnionTest { assertIs(decoded) } + @Test + fun `AutomationSchedule serializer uses wrapper discriminant`() { + val schedule: AutomationSchedule = AutomationScheduleDaily( + AutomationDailySchedule( + kind = AutomationScheduleKind.HOURLY, + time = AutomationLocalTime(hour = 9, minute = 30), + timeZone = "Europe/Berlin", + ), + ) + + val encoded = json.encodeToString(AutomationSchedule.serializer(), schedule) + assertEquals(JsonPrimitive("daily"), json.parseToJsonElement(encoded).jsonObject["kind"]) + } + @Test fun `ChatInputQuestion accepts both number and integer wire kinds`() { // Both "number" and "integer" wire values map to the same Kotlin diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt index a908a84c4..9853a2f8f 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt @@ -3,6 +3,8 @@ package com.microsoft.agenthostprotocol import com.microsoft.agenthostprotocol.generated.ChatState import com.microsoft.agenthostprotocol.generated.ChangesetState import com.microsoft.agenthostprotocol.generated.AnnotationsState +import com.microsoft.agenthostprotocol.generated.AutomationRunState +import com.microsoft.agenthostprotocol.generated.AutomationState import com.microsoft.agenthostprotocol.generated.ResourceWatchState import com.microsoft.agenthostprotocol.generated.RootState import com.microsoft.agenthostprotocol.generated.SessionState @@ -216,6 +218,29 @@ class FixtureDrivenReducerTest { }, ) + "automation" -> compareFixture( + file = file, + initial = initial, + expected = expected, + serializer = AutomationState.serializer(), + run = { state -> + var s = state + for (action in actions) s = automationReducer(s, action) + s + }, + ) + + "automationRun" -> compareFixture( + file = file, + initial = initial, + expected = expected, + serializer = AutomationRunState.serializer(), + run = { state -> + var s = state + for (action in actions) s = automationRunReducer(s, action) + s + }, + ) else -> fail("${file.name}: unsupported reducer '$reducer'") } diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt index d11abc8a3..7af1319f9 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt @@ -43,6 +43,7 @@ import com.microsoft.agenthostprotocol.generated.SessionAddedParams import com.microsoft.agenthostprotocol.generated.ChatInputQuestion import com.microsoft.agenthostprotocol.generated.SessionStatus import com.microsoft.agenthostprotocol.generated.SessionSummary +import com.microsoft.agenthostprotocol.generated.Snapshot import com.microsoft.agenthostprotocol.generated.StateAction import com.microsoft.agenthostprotocol.generated.StringOrMarkdown import java.io.File @@ -253,6 +254,7 @@ class RoundTripCorpusTest { "Implementation" -> rt(Implementation.serializer()) "InitializeResult" -> rt(InitializeResult.serializer()) "ChatSource" -> rt(ChatSource.serializer()) + "Snapshot" -> rt(Snapshot.serializer()) else -> fail( "$file: unknown wire type \"$typeName\". " + "Add a decode entry to decodeAndReencode.", diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 849a339c5..b810c25e5 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -13,11 +13,12 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; #[allow(unused_imports)] use crate::state::{ - AgentInfo, AgentSelection, Annotation, AnnotationEntry, Changeset, ChangesetFile, - ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer, - ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary, - ConfirmationOption, ContentRef, Customization, ErrorInfo, McpAuthRequirement, McpServerState, - Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, + AgentInfo, AgentSelection, Annotation, AnnotationEntry, AutomationDefinition, + AutomationRunArtifact, AutomationRunLifecycle, AutomationRunOperation, AutomationRunSummary, + Changeset, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, + ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, + ChatSummary, ConfirmationOption, ContentRef, Customization, ErrorInfo, McpAuthRequirement, + McpServerState, Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo, @@ -198,6 +199,28 @@ pub enum ActionType { TerminalCommandFinished, #[serde(rename = "resourceWatch/changed")] ResourceWatchChanged, + #[serde(rename = "automation/definitionChanged")] + AutomationDefinitionChanged, + #[serde(rename = "automation/runSummarySet")] + AutomationRunSummarySet, + #[serde(rename = "automation/runSummaryRemoved")] + AutomationRunSummaryRemoved, + #[serde(rename = "automation/runsLoaded")] + AutomationRunsLoaded, + #[serde(rename = "automationRun/lifecycleChanged")] + AutomationRunLifecycleChanged, + #[serde(rename = "automationRun/sessionSet")] + AutomationRunSessionSet, + #[serde(rename = "automationRun/sessionRemoved")] + AutomationRunSessionRemoved, + #[serde(rename = "automationRun/primarySessionChanged")] + AutomationRunPrimarySessionChanged, + #[serde(rename = "automationRun/artifactSet")] + AutomationRunArtifactSet, + #[serde(rename = "automationRun/artifactRemoved")] + AutomationRunArtifactRemoved, + #[serde(rename = "automationRun/cancelRequested")] + AutomationRunCancelRequested, } // ─── Action Envelope ───────────────────────────────────────────────── @@ -1736,6 +1759,78 @@ pub struct ResourceWatchChangedAction { pub changes: AnyValue, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationDefinitionChangedAction { + pub definition: AutomationDefinition, + pub revision: i64, + pub modified_at: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_run_at: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunSummarySetAction { + pub run: AutomationRunSummary, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunSummaryRemovedAction { + pub run: Uri, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunsLoadedAction { + pub runs: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunLifecycleChangedAction { + pub lifecycle: AutomationRunLifecycle, + pub operations: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunSessionSetAction { + pub session: Uri, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunSessionRemovedAction { + pub session: Uri, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunPrimarySessionChangedAction { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_session: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunArtifactSetAction { + pub artifact: AutomationRunArtifact, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunArtifactRemovedAction { + pub artifact_id: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunCancelRequestedAction {} + // ─── Partial Summaries ──────────────────────────────────────────────── /// Partial equivalent of ChatSummary — every field is optional for delta updates. @@ -1949,6 +2044,28 @@ pub enum StateAction { TerminalCommandFinished(TerminalCommandFinishedAction), #[serde(rename = "resourceWatch/changed")] ResourceWatchChanged(ResourceWatchChangedAction), + #[serde(rename = "automation/definitionChanged")] + AutomationDefinitionChanged(Box), + #[serde(rename = "automation/runSummarySet")] + AutomationRunSummarySet(Box), + #[serde(rename = "automation/runSummaryRemoved")] + AutomationRunSummaryRemoved(AutomationRunSummaryRemovedAction), + #[serde(rename = "automation/runsLoaded")] + AutomationRunsLoaded(Box), + #[serde(rename = "automationRun/lifecycleChanged")] + AutomationRunLifecycleChanged(Box), + #[serde(rename = "automationRun/sessionSet")] + AutomationRunSessionSet(AutomationRunSessionSetAction), + #[serde(rename = "automationRun/sessionRemoved")] + AutomationRunSessionRemoved(AutomationRunSessionRemovedAction), + #[serde(rename = "automationRun/primarySessionChanged")] + AutomationRunPrimarySessionChanged(AutomationRunPrimarySessionChangedAction), + #[serde(rename = "automationRun/artifactSet")] + AutomationRunArtifactSet(Box), + #[serde(rename = "automationRun/artifactRemoved")] + AutomationRunArtifactRemoved(AutomationRunArtifactRemovedAction), + #[serde(rename = "automationRun/cancelRequested")] + AutomationRunCancelRequested(AutomationRunCancelRequestedAction), /// 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-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 87869f685..f172886f3 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -15,9 +15,11 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::actions::{ActionEnvelope, StateAction}; #[allow(unused_imports)] use crate::state::{ - AgentSelection, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, - SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, - TelemetryCapabilities, TerminalClaim, TextRange, Turn, + AgentSelection, AutomationDefinition, AutomationExecutionLifetime, AutomationSchedule, + AutomationScheduleKind, AutomationSessionTemplate, AutomationSummary, AutomationTrigger, + AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, + SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, + SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn, }; // ─── Enums ──────────────────────────────────────────────────────────── @@ -195,6 +197,9 @@ pub struct InitializeResult { /// filtering). Clients MAY ignore signals they cannot process. #[serde(default, skip_serializing_if = "Option::is_none")] pub telemetry: Option, + /// Host automation support. Absence means unsupported. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub automations: Option, } /// Optional capabilities a client declares during `initialize`. @@ -220,6 +225,56 @@ pub struct ClientCapabilities { pub mcp_apps: Option, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationCapabilities { + pub execution: AutomationExecutionCapabilities, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub create: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schedules: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub run_cancellation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub schedule_preview: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub run_history_limit: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationExecutionCapabilities { + pub lifetime: AutomationExecutionLifetime, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationCreateCapability {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationScheduleCapabilities { + pub kinds: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cron: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationCronScheduleCapability { + pub dialect: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_interval_minutes: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunCancellationCapability {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationSchedulePreviewCapability {} + /// Identifies a protocol implementation — the software (and build) on one end /// of the connection, as distinct from the {@link AgentInfo | agent persona} it /// hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the @@ -1428,6 +1483,142 @@ pub struct ChangesetOperationFollowUp { pub external: Option, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListAutomationsParams { + /// Channel URI this command targets. + pub channel: Uri, + /// Maximum number of entries to return in this page. The server SHOULD respect + /// this bound but MAY return fewer entries and MAY impose its own upper cap. + /// Omit to let the server choose the page size. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + /// Omit to fetch the first page. Cursors are server-defined and MUST be treated + /// as opaque — do not parse, modify, or persist them across connections. An + /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cursor: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListAutomationsResult { + /// Opaque cursor for the next page. Present when more entries exist beyond the + /// returned page; absent signals the end of the collection. Pass it back as + /// {@link PaginatedParams.cursor} to fetch the following page. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + pub items: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListAutomationTriggerDefinitionsParams { + /// Channel URI this command targets. + pub channel: Uri, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub session_config: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListAutomationTriggerDefinitionsResult { + pub items: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAutomationParams { + /// Channel URI this command targets. + pub channel: Uri, + pub definition: AutomationDefinition, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub import: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AutomationDefinitionPatch { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub session: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub triggers: Option>, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateAutomationParams { + /// Channel URI this command targets. + pub channel: Uri, + pub expected_revision: i64, + pub changes: AutomationDefinitionPatch, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DisposeAutomationParams { + /// Channel URI this command targets. + pub channel: Uri, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RunAutomationParams { + /// Channel URI this command targets. + pub channel: Uri, + pub request_id: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RunAutomationResult { + pub run: Uri, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchAutomationRunsParams { + /// Channel URI this command targets. + pub channel: Uri, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cursor: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchAutomationRunsResult {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PreviewAutomationScheduleParams { + /// Channel URI this command targets. + pub channel: Uri, + pub schedule: AutomationSchedule, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub count: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PreviewAutomationScheduleResult { + pub items: Vec, +} + // ─── ChatSource Union ───────────────────────────────────────────────── /// How a new chat uses a source chat. diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index e54e93769..6d67cebb3 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -13,8 +13,9 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; #[allow(unused_imports)] use crate::state::{ - AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, - ProjectInfo, SessionStatus, SessionSummary, + AgentSelection, AnnotationsSummary, AutomationOperation, AutomationRunSummary, + AutomationSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, + SessionOrigin, SessionStatus, SessionSummary, }; // ─── Enums ──────────────────────────────────────────────────────────── @@ -96,6 +97,27 @@ pub struct SessionSummaryChangedParams { pub changes: PartialSessionSummary, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationAddedParams { + pub channel: Uri, + pub summary: AutomationSummary, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRemovedParams { + pub channel: Uri, + pub automation: Uri, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationSummaryChangedParams { + pub channel: Uri, + pub summary: AutomationSummary, +} + /// Generic progress notification for a long-running operation. /// /// A client opts in to progress for a request by including a `progressToken` in @@ -245,6 +267,9 @@ pub struct PartialSessionSummary { /// Human-readable description of what the session is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, + /// Durable origin of this session, when another AHP resource created it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub origin: Option, /// Server-owned project for this session #[serde(default, skip_serializing_if = "Option::is_none")] pub project: Option, diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 0e9f5cba7..10eeea8f3 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -587,6 +587,118 @@ pub enum ResourceChangeType { Deleted, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SessionOriginKind { + #[serde(rename = "automation")] + Automation, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationOperation { + #[serde(rename = "update")] + Update, + #[serde(rename = "dispose")] + Dispose, + #[serde(rename = "run")] + Run, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationExecutionLifetime { + #[serde(rename = "hostLifetime")] + HostLifetime, + #[serde(rename = "managed")] + Managed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationScheduleKind { + #[serde(rename = "hourly")] + Hourly, + #[serde(rename = "daily")] + Daily, + #[serde(rename = "weekly")] + Weekly, + #[serde(rename = "cron")] + Cron, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationWeekday { + #[serde(rename = "monday")] + Monday, + #[serde(rename = "tuesday")] + Tuesday, + #[serde(rename = "wednesday")] + Wednesday, + #[serde(rename = "thursday")] + Thursday, + #[serde(rename = "friday")] + Friday, + #[serde(rename = "saturday")] + Saturday, + #[serde(rename = "sunday")] + Sunday, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationMisfirePolicy { + #[serde(rename = "skip")] + Skip, + #[serde(rename = "runOnce")] + RunOnce, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationTriggerKind { + #[serde(rename = "schedule")] + Schedule, + #[serde(rename = "event")] + Event, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationRunStatus { + #[serde(rename = "pending")] + Pending, + #[serde(rename = "running")] + Running, + #[serde(rename = "blocked")] + Blocked, + #[serde(rename = "completed")] + Completed, + #[serde(rename = "failed")] + Failed, + #[serde(rename = "cancelled")] + Cancelled, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationRunBlockerKind { + #[serde(rename = "userInput")] + UserInput, + #[serde(rename = "toolConfirmation")] + ToolConfirmation, + #[serde(rename = "authentication")] + Authentication, + #[serde(rename = "clientExecution")] + ClientExecution, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationRunCauseKind { + #[serde(rename = "manual")] + Manual, + #[serde(rename = "trigger")] + Trigger, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AutomationRunOperation { + #[serde(rename = "cancel")] + Cancel, +} + // ─── Structs ────────────────────────────────────────────────────────── /// An optionally-sized icon that can be displayed in a user interface. @@ -1150,6 +1262,9 @@ pub struct SessionState { /// Human-readable description of what the session is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, + /// Durable origin of this session, when another AHP resource created it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub origin: Option, /// Server-owned project for this session #[serde(default, skip_serializing_if = "Option::is_none")] pub project: Option, @@ -1446,6 +1561,9 @@ pub struct SessionSummary { /// Human-readable description of what the session is currently doing #[serde(default, skip_serializing_if = "Option::is_none")] pub activity: Option, + /// Durable origin of this session, when another AHP resource created it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub origin: Option, /// Server-owned project for this session #[serde(default, skip_serializing_if = "Option::is_none")] pub project: Option, @@ -4258,6 +4376,298 @@ pub struct ResourceChange { pub r#type: ResourceChangeType, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationSessionOrigin { + pub automation: Uri, + pub run: Uri, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationLocalTime { + pub hour: i64, + pub minute: i64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationHourlySchedule {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationDailySchedule { + pub time: AutomationLocalTime, + /// IANA time-zone identifier. + pub time_zone: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationWeeklySchedule { + pub weekday: AutomationWeekday, + pub time: AutomationLocalTime, + /// IANA time-zone identifier. + pub time_zone: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationCronSchedule { + /// Standard five-field Unix cron expression. + pub expression: String, + /// IANA time-zone identifier. + pub time_zone: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationScheduleTrigger { + /// Stable within the automation definition. + pub id: String, + pub schedule: AutomationSchedule, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub misfire_policy: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationEventTrigger { + /// Stable within the automation definition. + pub id: String, + /// Stable host-defined trigger type. + pub r#type: String, + /// Selected event actions. + pub events: Vec, + /// Schema-defined values. Unknown entries must survive round-trips. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationTriggerEventDefinition { + pub id: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationTriggerDefinition { + pub r#type: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub events: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config_schema: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AutomationSessionTemplate { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + /// Absence means a workspace-less session. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, + /// Values resolved through `resolveSessionConfig`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationDefinition { + pub title: String, + /// Initial user message sent to each new session. + pub message: Message, + pub session: AutomationSessionTemplate, + /// Controls automatic triggers; manual runs remain permitted. + pub enabled: bool, + /// Empty means manual-only. + pub triggers: Vec, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRuntimeState { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub working_directories: Option>, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationSummary { + pub resource: Uri, + pub title: String, + pub enabled: bool, + pub trigger_count: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_run_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_run: Option, + pub revision: i64, + pub operations: Vec, + pub created_at: String, + pub modified_at: String, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationState { + pub resource: Uri, + pub definition: AutomationDefinition, + pub revision: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_run_at: Option, + /// Newest-first retained run summaries. + pub runs: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runs_next_cursor: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime: Option, + pub operations: Vec, + pub created_at: String, + pub modified_at: String, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunBlocker { + pub kind: AutomationRunBlockerKind, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationManualRunCause {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationTriggeredRunCause { + pub trigger_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scheduled_for: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub catch_up: Option, + /// Host-defined event provenance containing no secrets. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub event: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationPendingRunLifecycle { + pub created_at: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunningRunLifecycle { + pub created_at: String, + pub started_at: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationBlockedRunLifecycle { + pub created_at: String, + pub started_at: String, + pub blocker: AutomationRunBlocker, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationCompletedRunLifecycle { + pub created_at: String, + pub started_at: String, + pub completed_at: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub usage: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationFailedRunLifecycle { + pub created_at: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub started_at: Option, + pub completed_at: String, + pub error: ErrorInfo, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationCancelledRunLifecycle { + pub created_at: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub started_at: Option, + pub completed_at: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunArtifact { + /// Content URI + pub uri: Uri, + /// Approximate size in bytes + #[serde(default, skip_serializing_if = "Option::is_none")] + pub size_hint: Option, + /// Content MIME type + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_type: Option, + /// Content nonce + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nonce: Option, + pub id: String, + pub label: String, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunSummary { + pub resource: Uri, + pub automation: Uri, + pub cause: AutomationRunCause, + pub lifecycle: AutomationRunLifecycle, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_session: Option, + pub session_count: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub artifact_count: Option, + pub operations: Vec, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationRunState { + pub resource: Uri, + pub automation: Uri, + pub cause: AutomationRunCause, + pub lifecycle: AutomationRunLifecycle, + pub sessions: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub primary_session: Option, + pub artifacts: Vec, + pub operations: Vec, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + /// Raw tool input represented inline or by content reference. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] @@ -4621,8 +5031,67 @@ pub enum SessionInputRequest { Unknown(serde_json::Value), } -/// The state payload of a snapshot — root, session, chat, terminal, -/// changeset, resource-watch, annotations, or content state. +/// Durable origin of a session. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum SessionOrigin { + #[serde(rename = "automation")] + Automation(AutomationSessionOrigin), +} + +/// Calendar schedule for an automation trigger. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum AutomationSchedule { + #[serde(rename = "hourly")] + Hourly(AutomationHourlySchedule), + #[serde(rename = "daily")] + Daily(AutomationDailySchedule), + #[serde(rename = "weekly")] + Weekly(AutomationWeeklySchedule), + #[serde(rename = "cron")] + Cron(AutomationCronSchedule), +} + +/// Automatic trigger for an automation. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum AutomationTrigger { + #[serde(rename = "schedule")] + Schedule(AutomationScheduleTrigger), + #[serde(rename = "event")] + Event(AutomationEventTrigger), +} + +/// Cause of an automation run. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum AutomationRunCause { + #[serde(rename = "manual")] + Manual(AutomationManualRunCause), + #[serde(rename = "trigger")] + Trigger(AutomationTriggeredRunCause), +} + +/// Lifecycle of an automation run. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "status")] +pub enum AutomationRunLifecycle { + #[serde(rename = "pending")] + Pending(AutomationPendingRunLifecycle), + #[serde(rename = "running")] + Running(AutomationRunningRunLifecycle), + #[serde(rename = "blocked")] + Blocked(AutomationBlockedRunLifecycle), + #[serde(rename = "completed")] + Completed(AutomationCompletedRunLifecycle), + #[serde(rename = "failed")] + Failed(AutomationFailedRunLifecycle), + #[serde(rename = "cancelled")] + Cancelled(AutomationCancelledRunLifecycle), +} + +/// The state payload of a snapshot. /// /// Deserialized by trying session first (has required `lifecycle`), then /// chat (has required `turns`), then terminal (has required `content`), @@ -4638,5 +5107,7 @@ pub enum SnapshotState { Changeset(Box), ResourceWatch(Box), Annotations(Box), + Automation(Box), + AutomationRun(Box), Root(Box), } diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs index 030a4720a..b9d50a21d 100644 --- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs +++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs @@ -30,7 +30,7 @@ use ahp_types::{ common::StringOrMarkdown, messages::JsonRpcMessage, notifications::{PartialSessionSummary, SessionAddedParams}, - state::{ChatInputQuestion, Customization, SessionStatus, SessionSummary}, + state::{ChatInputQuestion, Customization, SessionStatus, SessionSummary, Snapshot}, version::{PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS}, }; use serde_json::{Number, Value}; @@ -222,6 +222,7 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result< "Implementation" => round_trip!(Implementation), "InitializeResult" => round_trip!(InitializeResult), "ChatSource" => round_trip!(ChatSource), + "Snapshot" => round_trip!(Snapshot), other => Err(format!( "{}: unknown wire type {:?}. Add a decode entry to decode_and_reencode.", file, other diff --git a/clients/rust/crates/ahp/src/client.rs b/clients/rust/crates/ahp/src/client.rs index b5547d0ad..285360937 100644 --- a/clients/rust/crates/ahp/src/client.rs +++ b/clients/rust/crates/ahp/src/client.rs @@ -49,7 +49,9 @@ use ahp_types::messages::{ JsonRpcNotification, JsonRpcRequest, JsonRpcSuccessResponse, JsonRpcVersion, }; use ahp_types::notifications::{ - AuthRequiredParams, SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, + AuthRequiredParams, AutomationAddedParams, AutomationRemovedParams, + AutomationSummaryChangedParams, SessionAddedParams, SessionRemovedParams, + SessionSummaryChangedParams, }; use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; @@ -86,8 +88,9 @@ impl Default for ClientConfig { /// /// `Action` envelopes carry the write-ahead mutation stream; the /// remaining variants carry per-channel protocol notifications the -/// server emits as top-level JSON-RPC methods (session catalogue events -/// on the root channel, auth-required signals scoped to a channel). +/// server emits as top-level JSON-RPC methods (session and automation +/// catalogue events on the root channel, auth-required signals scoped to +/// a channel). #[derive(Debug, Clone)] #[non_exhaustive] pub enum SubscriptionEvent { @@ -99,6 +102,12 @@ pub enum SubscriptionEvent { SessionRemoved(SessionRemovedParams), /// `root/sessionSummaryChanged`: a session summary mutated. SessionSummaryChanged(SessionSummaryChangedParams), + /// `root/automationAdded`: a new automation was added to the catalogue. + AutomationAdded(AutomationAddedParams), + /// `root/automationRemoved`: an automation was removed from the catalogue. + AutomationRemoved(AutomationRemovedParams), + /// `root/automationSummaryChanged`: an automation summary mutated. + AutomationSummaryChanged(AutomationSummaryChangedParams), /// `auth/required`: the server needs (re-)authentication for a channel. AuthRequired(AuthRequiredParams), } @@ -990,6 +999,35 @@ async fn handle_notification(shared: &Shared, n: JsonRpcNotification) { .await; } } + "root/automationAdded" => { + if let Ok(params) = serde_json::from_value::(params_val) { + let channel = params.channel.clone(); + fan_out(shared, &channel, SubscriptionEvent::AutomationAdded(params)).await; + } + } + "root/automationRemoved" => { + if let Ok(params) = serde_json::from_value::(params_val) { + let channel = params.channel.clone(); + fan_out( + shared, + &channel, + SubscriptionEvent::AutomationRemoved(params), + ) + .await; + } + } + "root/automationSummaryChanged" => { + if let Ok(params) = serde_json::from_value::(params_val) + { + let channel = params.channel.clone(); + fan_out( + shared, + &channel, + SubscriptionEvent::AutomationSummaryChanged(params), + ) + .await; + } + } "auth/required" => { if let Ok(params) = serde_json::from_value::(params_val) { let channel = params.channel.clone(); diff --git a/clients/rust/crates/ahp/src/hosts/runtime.rs b/clients/rust/crates/ahp/src/hosts/runtime.rs index 3baf43edc..d46cc343f 100644 --- a/clients/rust/crates/ahp/src/hosts/runtime.rs +++ b/clients/rust/crates/ahp/src/hosts/runtime.rs @@ -93,6 +93,7 @@ pub(super) fn spawn( protocol_version: None, server_seq: 0, default_directory: None, + automations: None, root_state: RootState { agents: vec![], active_sessions: None, @@ -346,6 +347,7 @@ impl HostRuntime { } state.protocol_version = Some(init.protocol_version.clone()); state.default_directory = init.default_directory.clone(); + state.automations = init.automations.clone(); state.completion_trigger_characters = init .completion_trigger_characters .clone() @@ -578,6 +580,11 @@ impl HostRuntime { apply_summary_changes(existing, &n.changes); } } + SubscriptionEvent::AutomationAdded(_) + | SubscriptionEvent::AutomationRemoved(_) + | SubscriptionEvent::AutomationSummaryChanged(_) => { + // No cache update; consumers observe via the event stream. + } SubscriptionEvent::AuthRequired(_) => { // No cache update; consumers observe via the event stream. } @@ -717,6 +724,9 @@ fn apply_summary_changes( if let Some(v) = &changes.activity { existing.activity = Some(v.clone()); } + if let Some(v) = &changes.origin { + existing.origin = Some(v.clone()); + } if let Some(v) = &changes.modified_at { existing.modified_at = v.clone(); } diff --git a/clients/rust/crates/ahp/src/hosts/types.rs b/clients/rust/crates/ahp/src/hosts/types.rs index 8846486e6..ec47aaa12 100644 --- a/clients/rust/crates/ahp/src/hosts/types.rs +++ b/clients/rust/crates/ahp/src/hosts/types.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use std::time::SystemTime; use ahp_types::actions::ActionEnvelope; +use ahp_types::commands::AutomationCapabilities; use ahp_types::state::{AgentInfo, RootState, SessionSummary, TerminalInfo}; use thiserror::Error; use tokio::sync::{broadcast, Mutex}; @@ -202,9 +203,10 @@ impl std::fmt::Debug for HostConfig { /// Snapshot of everything the multi-host SDK knows about a single host. /// /// This is the value type UIs render: connection state, last error, -/// protocol version, agents pulled from root state, subscribed URIs, -/// cached session summaries, and so on. Cheap to clone (most fields -/// are small or already `Arc`-shared internally). +/// protocol version, host automation capabilities, agents pulled from +/// root state, subscribed URIs, cached session summaries, and so on. +/// Cheap to clone (most fields are small or already `Arc`-shared +/// internally). /// /// Snapshots are immutable; refresh by calling [`super::MultiHostClient::host`] /// or [`super::MultiHostClient::hosts`] again, or subscribe to the @@ -235,6 +237,8 @@ pub struct HostHandle { pub server_seq: i64, /// Optional `defaultDirectory` from the host's `InitializeResult`. pub default_directory: Option, + /// Automation support advertised by the host. + pub automations: Option, /// Agents currently advertised by the host (mirrored from root state). pub agents: Vec, /// Active session count from root state, when present. @@ -543,6 +547,7 @@ pub(super) struct HostInternal { pub(super) protocol_version: Option, pub(super) server_seq: i64, pub(super) default_directory: Option, + pub(super) automations: Option, pub(super) root_state: RootState, pub(super) subscriptions: Vec, pub(super) completion_trigger_characters: Vec, @@ -563,6 +568,7 @@ impl HostInternal { protocol_version: self.protocol_version.clone(), server_seq: self.server_seq, default_directory: self.default_directory.clone(), + automations: self.automations.clone(), agents: self.root_state.agents.clone(), active_sessions: self.root_state.active_sessions, terminals: self.root_state.terminals.clone(), diff --git a/clients/rust/crates/ahp/src/lib.rs b/clients/rust/crates/ahp/src/lib.rs index 1511bc9ec..463339852 100644 --- a/clients/rust/crates/ahp/src/lib.rs +++ b/clients/rust/crates/ahp/src/lib.rs @@ -161,6 +161,7 @@ pub use client::{ pub use error::{ClientError, TransportError}; pub use multi_host_state_mirror::{HostedResourceKey, MultiHostStateMirror}; pub use reducers::{ - apply_action_to_root, apply_action_to_session, apply_action_to_terminal, ReduceOutcome, + apply_action_to_automation, apply_action_to_automation_run, apply_action_to_root, + apply_action_to_session, apply_action_to_terminal, ReduceOutcome, }; pub use transport::{BoxedTransport, DynTransport, Transport, TransportMessage}; diff --git a/clients/rust/crates/ahp/src/multi_host_state_mirror.rs b/clients/rust/crates/ahp/src/multi_host_state_mirror.rs index 7288894b9..4ae78e183 100644 --- a/clients/rust/crates/ahp/src/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/src/multi_host_state_mirror.rs @@ -1,10 +1,12 @@ //! Host-aware reducer façade for multi-host consumers. //! -//! Wraps the existing pure reducers -//! ([`apply_action_to_root`](crate::reducers::apply_action_to_root), +//! Wraps the existing pure reducers: +//! [`apply_action_to_automation`](crate::reducers::apply_action_to_automation), +//! [`apply_action_to_automation_run`](crate::reducers::apply_action_to_automation_run), +//! [`apply_action_to_root`](crate::reducers::apply_action_to_root), //! [`apply_action_to_session`](crate::reducers::apply_action_to_session), -//! [`apply_action_to_terminal`](crate::reducers::apply_action_to_terminal)) -//! the way a single-host consumer would, but keys state by +//! and [`apply_action_to_terminal`](crate::reducers::apply_action_to_terminal). +//! It applies them the way a single-host consumer would, but keys state by //! `(host_id, uri)` so resource URIs that legitimately collide across //! hosts (the normal case for session URIs) don't clobber each other. //! @@ -37,13 +39,14 @@ use std::collections::HashMap; use ahp_types::actions::ActionEnvelope; use ahp_types::common::ROOT_RESOURCE_URI; use ahp_types::state::{ - AnnotationsState, ChangesetState, ChatState, ResourceWatchState, RootState, SessionState, - SnapshotState, TerminalState, + AnnotationsState, AutomationRunState, AutomationState, ChangesetState, ChatState, + ResourceWatchState, RootState, SessionState, SnapshotState, TerminalState, }; use crate::hosts::{HostId, HostSubscriptionEvent}; use crate::reducers::{ - apply_action_to_chat, apply_action_to_root, apply_action_to_session, apply_action_to_terminal, + apply_action_to_automation, apply_action_to_automation_run, apply_action_to_chat, + apply_action_to_root, apply_action_to_session, apply_action_to_terminal, }; use crate::SubscriptionEvent; @@ -71,7 +74,7 @@ impl HostedResourceKey { } } -/// In-memory mirror of per-host root/session/terminal/changeset state, +/// In-memory mirror of per-host root/session/terminal/changeset/automation state, /// fed by [`ActionEnvelope`]s and snapshot states tagged with their /// host of origin. /// @@ -92,6 +95,8 @@ pub struct MultiHostStateMirror { changesets: HashMap, annotations: HashMap, resource_watches: HashMap, + automations: HashMap, + automation_runs: HashMap, } impl MultiHostStateMirror { @@ -135,11 +140,21 @@ impl MultiHostStateMirror { &self.resource_watches } + /// Borrow automation states keyed by `(host_id, uri)`. + pub fn automations(&self) -> &HashMap { + &self.automations + } + + /// Borrow automation-run states keyed by `(host_id, uri)`. + pub fn automation_runs(&self) -> &HashMap { + &self.automation_runs + } + /// Convenience: apply a [`HostSubscriptionEvent`] produced by /// [`crate::hosts::MultiHostClient::events`]. Action envelopes are /// routed through the reducer; non-action events (session-summary - /// notifications, auth challenges) are ignored — they don't move - /// any of the reducer-tracked state shapes. + /// notifications, automation-catalogue notifications, auth challenges) + /// are ignored — they don't move any of the reducer-tracked state shapes. pub fn apply_event(&mut self, event: &HostSubscriptionEvent) { if let SubscriptionEvent::Action(envelope) = &event.event { self.apply_envelope(&event.host_id, envelope); @@ -176,6 +191,14 @@ impl MultiHostStateMirror { } if let Some(terminal) = self.terminals.get_mut(&key) { apply_action_to_terminal(terminal, &envelope.action); + return; + } + if let Some(automation) = self.automations.get_mut(&key) { + apply_action_to_automation(automation, &envelope.action); + return; + } + if let Some(run) = self.automation_runs.get_mut(&key) { + apply_action_to_automation_run(run, &envelope.action); } // Changesets are seeded by `apply_snapshot` only — there's no // changeset reducer in the SDK today (matching the Swift @@ -184,8 +207,8 @@ impl MultiHostStateMirror { /// Seed the mirror from a [`Snapshot`](ahp_types::state::Snapshot) /// scoped to `host` — root, session, terminal, changeset, - /// resource-watch, or annotations as the snapshot's `state` - /// discriminator dictates. + /// resource-watch, annotations, automation, or automation-run as the + /// snapshot's `state` discriminator dictates. pub fn apply_snapshot(&mut self, host: &HostId, snapshot: &ahp_types::state::Snapshot) { let key = HostedResourceKey::new(host.clone(), snapshot.resource.clone()); match &snapshot.state { @@ -211,11 +234,18 @@ impl MultiHostStateMirror { SnapshotState::Annotations(state) => { self.annotations.insert(key, state.as_ref().clone()); } + SnapshotState::Automation(state) => { + self.automations.insert(key, state.as_ref().clone()); + } + SnapshotState::AutomationRun(state) => { + self.automation_runs.insert(key, state.as_ref().clone()); + } } } /// Drop every slot keyed under `host` — root state, sessions, - /// terminals, changesets, resource watches, and annotations. + /// terminals, changesets, resource watches, annotations, automations, + /// and automation runs. pub fn reset_host(&mut self, host: &HostId) { self.root_states.remove(host); self.sessions.retain(|key, _| &key.host_id != host); @@ -224,6 +254,8 @@ impl MultiHostStateMirror { self.changesets.retain(|key, _| &key.host_id != host); self.annotations.retain(|key, _| &key.host_id != host); self.resource_watches.retain(|key, _| &key.host_id != host); + self.automations.retain(|key, _| &key.host_id != host); + self.automation_runs.retain(|key, _| &key.host_id != host); } /// Drop every host's state. @@ -235,5 +267,7 @@ impl MultiHostStateMirror { self.changesets.clear(); self.annotations.clear(); self.resource_watches.clear(); + self.automations.clear(); + self.automation_runs.clear(); } } diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 027f48e06..b860f6ed2 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -58,16 +58,17 @@ use ahp_types::actions::{ ChatTurnStartedAction, StateAction, }; use ahp_types::state::{ - ActiveTurn, AnnotationsState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, - ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, ErrorInfo, - InputRequestResponsePart, McpServerStartingState, McpServerState, McpServerStoppedState, - PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, RootState, - SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, - TerminalContentPart, TerminalState, TerminalUnclassifiedPart, ToolCallAuthRequiredState, - ToolCallCancellationReason, ToolCallCancelledState, ToolCallCompletedState, - ToolCallConfirmationReason, ToolCallContributor, ToolCallPendingConfirmationState, - ToolCallPendingResultConfirmationState, ToolCallResponsePart, ToolCallRunningState, - ToolCallState, ToolCallStatus, ToolCallStreamingState, ToolInput, Turn, TurnState, + ActiveTurn, AnnotationsState, AutomationRunState, AutomationState, ChangesetOperationStatus, + ChangesetState, ChangesetStatus, ChatInputRequest, ChatState, ChildCustomization, + ConfirmationOption, Customization, ErrorInfo, InputRequestResponsePart, McpServerStartingState, + McpServerState, McpServerStoppedState, PendingMessage, PendingMessageKind, ResourceWatchState, + ResponsePart, RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, + TerminalCommandPart, TerminalContentPart, TerminalState, TerminalUnclassifiedPart, + ToolCallAuthRequiredState, ToolCallCancellationReason, ToolCallCancelledState, + ToolCallCompletedState, ToolCallConfirmationReason, ToolCallContributor, + ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, ToolCallResponsePart, + ToolCallRunningState, ToolCallState, ToolCallStatus, ToolCallStreamingState, ToolInput, Turn, + TurnState, }; /// What happened when an action was applied. @@ -1961,6 +1962,116 @@ pub fn apply_action_to_resource_watch( } } +/// Apply a [`StateAction`] to an [`AutomationState`] in place. +pub fn apply_action_to_automation( + state: &mut AutomationState, + action: &StateAction, +) -> ReduceOutcome { + match action { + StateAction::AutomationDefinitionChanged(a) => { + state.definition = a.definition.clone(); + state.revision = a.revision; + state.modified_at = a.modified_at.clone(); + state.next_run_at = a.next_run_at.clone(); + ReduceOutcome::Applied + } + StateAction::AutomationRunSummarySet(a) => { + if let Some(index) = state + .runs + .iter() + .position(|run| run.resource == a.run.resource) + { + state.runs[index] = a.run.clone(); + } else { + state.runs.insert(0, a.run.clone()); + } + ReduceOutcome::Applied + } + StateAction::AutomationRunSummaryRemoved(a) => { + let Some(index) = state.runs.iter().position(|run| run.resource == a.run) else { + return ReduceOutcome::NoOp; + }; + state.runs.remove(index); + ReduceOutcome::Applied + } + StateAction::AutomationRunsLoaded(a) => { + let mut known: HashSet<_> = state.runs.iter().map(|run| run.resource.clone()).collect(); + for run in &a.runs { + if known.insert(run.resource.clone()) { + state.runs.push(run.clone()); + } + } + state.runs_next_cursor = a.next_cursor.clone(); + ReduceOutcome::Applied + } + _ => ReduceOutcome::OutOfScope, + } +} + +/// Apply a [`StateAction`] to an [`AutomationRunState`] in place. +pub fn apply_action_to_automation_run( + state: &mut AutomationRunState, + action: &StateAction, +) -> ReduceOutcome { + match action { + StateAction::AutomationRunLifecycleChanged(a) => { + state.lifecycle = a.lifecycle.clone(); + state.operations = a.operations.clone(); + ReduceOutcome::Applied + } + StateAction::AutomationRunSessionSet(a) => { + if state.sessions.contains(&a.session) { + return ReduceOutcome::NoOp; + } + state.sessions.push(a.session.clone()); + ReduceOutcome::Applied + } + StateAction::AutomationRunSessionRemoved(a) => { + let Some(index) = state + .sessions + .iter() + .position(|session| session == &a.session) + else { + return ReduceOutcome::NoOp; + }; + state.sessions.remove(index); + if state.primary_session.as_ref() == Some(&a.session) { + state.primary_session = None; + } + ReduceOutcome::Applied + } + StateAction::AutomationRunPrimarySessionChanged(a) => { + state.primary_session = a.primary_session.clone(); + ReduceOutcome::Applied + } + StateAction::AutomationRunArtifactSet(a) => { + if let Some(index) = state + .artifacts + .iter() + .position(|artifact| artifact.id == a.artifact.id) + { + state.artifacts[index] = a.artifact.clone(); + } else { + state.artifacts.push(a.artifact.clone()); + } + ReduceOutcome::Applied + } + StateAction::AutomationRunArtifactRemoved(a) => { + let Some(index) = state + .artifacts + .iter() + .position(|artifact| artifact.id == a.artifact_id) + else { + return ReduceOutcome::NoOp; + }; + state.artifacts.remove(index); + ReduceOutcome::Applied + } + StateAction::AutomationRunCancelRequested(_) => ReduceOutcome::NoOp, + _ => ReduceOutcome::OutOfScope, + } +} + #[cfg(test)] mod tests { use super::*; @@ -1987,6 +2098,7 @@ mod tests { title: String::new(), status: SessionStatus::Idle.bits(), activity: None, + origin: None, project: None, working_directories: None, annotations: None, @@ -2411,6 +2523,22 @@ mod tests { &file_name, description, ), + "automation" => run_fixture::( + initial, + expected, + &parsed_actions, + apply_action_to_automation, + &file_name, + description, + ), + "automationRun" => run_fixture::( + initial, + expected, + &parsed_actions, + apply_action_to_automation_run, + &file_name, + description, + ), other => { panic!("{file_name}: unknown reducer type '{other}'"); } diff --git a/clients/rust/crates/ahp/tests/client_roundtrip.rs b/clients/rust/crates/ahp/tests/client_roundtrip.rs index fdc265e8b..90cf533d7 100644 --- a/clients/rust/crates/ahp/tests/client_roundtrip.rs +++ b/clients/rust/crates/ahp/tests/client_roundtrip.rs @@ -156,6 +156,99 @@ async fn request_response_and_action_fanout() { server.await.unwrap(); } +#[tokio::test] +async fn automation_catalogue_notifications_fan_out() { + let (client_side, mut server_side) = pair(); + let client = Client::connect(client_side, ClientConfig::default()) + .await + .expect("connect"); + let mut subscription = client + .attach_subscription(ahp_types::ROOT_RESOURCE_URI) + .await; + + let summary = serde_json::json!({ + "resource": "ahp-automation:/a1", + "title": "Nightly triage", + "enabled": true, + "triggerCount": 1, + "revision": 2, + "operations": ["update", "dispose", "run"], + "createdAt": "2026-08-01T00:00:00Z", + "modifiedAt": "2026-08-05T12:00:00Z" + }); + for (method, params) in [ + ( + "root/automationAdded", + serde_json::json!({ + "channel": ahp_types::ROOT_RESOURCE_URI, + "summary": summary, + }), + ), + ( + "root/automationSummaryChanged", + serde_json::json!({ + "channel": ahp_types::ROOT_RESOURCE_URI, + "summary": { + "resource": "ahp-automation:/a1", + "title": "Updated triage", + "enabled": false, + "triggerCount": 1, + "revision": 3, + "operations": ["update", "dispose", "run"], + "createdAt": "2026-08-01T00:00:00Z", + "modifiedAt": "2026-08-05T13:00:00Z" + }, + }), + ), + ( + "root/automationRemoved", + serde_json::json!({ + "channel": ahp_types::ROOT_RESOURCE_URI, + "automation": "ahp-automation:/a1", + }), + ), + ] { + let notification = JsonRpcMessage::Notification(JsonRpcNotification { + jsonrpc: JsonRpcVersion::V2, + method: method.into(), + params: Some(ahp_types::common::AnyValue::from(params)), + }); + server_side + .send(TransportMessage::encode(¬ification).unwrap()) + .await + .unwrap(); + } + + let added = tokio::time::timeout(std::time::Duration::from_secs(2), subscription.recv()) + .await + .expect("timed out") + .expect("channel closed"); + let added = match added { + SubscriptionEvent::AutomationAdded(params) => params, + other => panic!("expected AutomationAdded, got {other:?}"), + }; + assert_eq!(added.summary.resource, "ahp-automation:/a1"); + assert_eq!(added.summary.title, "Nightly triage"); + + let changed = subscription.recv().await.expect("channel closed"); + let changed = match changed { + SubscriptionEvent::AutomationSummaryChanged(params) => params, + other => panic!("expected AutomationSummaryChanged, got {other:?}"), + }; + assert_eq!(changed.summary.resource, "ahp-automation:/a1"); + assert_eq!(changed.summary.title, "Updated triage"); + assert!(!changed.summary.enabled); + + let removed = subscription.recv().await.expect("channel closed"); + let removed = match removed { + SubscriptionEvent::AutomationRemoved(params) => params, + other => panic!("expected AutomationRemoved, got {other:?}"), + }; + assert_eq!(removed.automation, "ahp-automation:/a1"); + + client.shutdown().await; +} + #[tokio::test] async fn resource_read_send_wrapper_targets_root_channel() { use ahp_types::commands::{ContentEncoding, ResourceReadParams}; diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs index 9031a7658..a902e5fbc 100644 --- a/clients/rust/crates/ahp/tests/hosts.rs +++ b/clients/rust/crates/ahp/tests/hosts.rs @@ -17,10 +17,11 @@ use ahp::hosts::{ }; use ahp::transport::BoxedTransport; use ahp::{Transport, TransportError, TransportMessage}; +use ahp_types::commands::{AutomationCapabilities, AutomationExecutionCapabilities}; use ahp_types::messages::{ JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcSuccessResponse, JsonRpcVersion, }; -use ahp_types::state::AgentInfo; +use ahp_types::state::{AgentInfo, AutomationExecutionLifetime}; use tokio::sync::{mpsc, Mutex}; // ─── In-memory transport ──────────────────────────────────────────────────── @@ -59,6 +60,8 @@ struct FakeHostState { agents: Vec, /// Optional list of session summaries to return from `listSessions`. sessions: Vec, + /// Automation support to advertise in `InitializeResult`. + automations: Option, } impl FakeHostState { @@ -67,6 +70,7 @@ impl FakeHostState { server_seq: Arc::new(AtomicU32::new(0)), agents: vec![], sessions: vec![], + automations: None, } } @@ -79,6 +83,11 @@ impl FakeHostState { self.sessions = sessions; self } + + fn with_automations(mut self, automations: AutomationCapabilities) -> Self { + self.automations = Some(automations); + self + } } /// Drive a single connection on the server side until the client closes. @@ -187,6 +196,7 @@ fn handle_request(req: &JsonRpcRequest, state: &FakeHostState) -> serde_json::Va "protocolVersion": ahp_types::PROTOCOL_VERSION, "serverSeq": seq, "snapshots": [snapshot], + "automations": state.automations, }) } "reconnect" => serde_json::json!({ @@ -372,6 +382,53 @@ async fn host_client_handle_invalidates_after_reconnect() { fresh.check_alive().await.expect("fresh handle alive"); } +#[tokio::test] +async fn automation_capabilities_are_exposed_and_survive_reconnect() { + let automations = AutomationCapabilities { + execution: AutomationExecutionCapabilities { + lifetime: AutomationExecutionLifetime::Managed, + }, + create: None, + schedules: None, + run_cancellation: None, + schedule_preview: None, + run_history_limit: Some(25), + }; + let drop_after_init = Arc::new(AtomicBool::new(false)); + let return_replay = Arc::new(Mutex::new(true)); + let state = FakeHostState::new().with_automations(automations.clone()); + let multi = MultiHostClient::new(); + multi + .add_host( + HostConfig::new( + "local", + "Local", + make_replay_factory(state, drop_after_init, return_replay), + ) + .with_reconnect_policy(ReconnectPolicy::immediate_forever()), + ) + .await + .unwrap(); + + let host_id = HostId::new("local"); + wait_for_state(&multi, &host_id, |s| s.is_connected(), 2000).await; + let before = multi.host(&host_id).await.expect("host"); + assert_eq!(before.automations, Some(automations.clone())); + + multi.reconnect_host(&host_id).await.expect("reconnect"); + wait_until(2000, || async { + multi + .host(&host_id) + .await + .map(|host| host.generation > before.generation && host.state.is_connected()) + .unwrap_or(false) + }) + .await; + + let after = multi.host(&host_id).await.expect("host"); + assert_eq!(after.automations, Some(automations)); +} + #[tokio::test] async fn remove_host_terminates_supervisor_and_emits_event() { let factory = make_basic_factory(FakeHostState::new()); @@ -1036,6 +1093,7 @@ fn make_summary(uri: &str, title: &str, modified_at: i64) -> ahp_types::state::S title: title.into(), status: 0, activity: None, + origin: None, created_at: "1970-01-01T00:00:00.000Z".into(), modified_at: modified, project: None, diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index 23969f940..44d9ad157 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -50,6 +50,7 @@ fn session_state(title: &str, _resource: &str) -> SessionState { title: title.into(), status: SessionStatus::Idle.bits(), activity: None, + origin: None, project: None, working_directories: None, annotations: None, @@ -75,6 +76,49 @@ fn session_snapshot(title: &str, resource: &str) -> Snapshot { } } +fn automation_snapshot() -> Snapshot { + serde_json::from_value(serde_json::json!({ + "resource": "ahp-automation:/a1", + "state": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "Old", + "message": { "text": "triage", "origin": { "kind": "user" } }, + "session": {}, + "enabled": true, + "triggers": [] + }, + "revision": 1, + "runs": [], + "operations": ["update", "run"], + "createdAt": "2026-08-01T00:00:00Z", + "modifiedAt": "2026-08-01T00:00:00Z" + }, + "fromSeq": 1 + })) + .expect("automation snapshot") +} + +fn automation_run_snapshot() -> Snapshot { + serde_json::from_value(serde_json::json!({ + "resource": "ahp-automation-run:/r1", + "state": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "pending", + "createdAt": "2026-08-05T12:00:00Z" + }, + "sessions": [], + "artifacts": [], + "operations": ["cancel"] + }, + "fromSeq": 2 + })) + .expect("automation-run snapshot") +} + fn root_agents_changed_envelope(agents: Vec, server_seq: u64) -> ActionEnvelope { ActionEnvelope { channel: ROOT_RESOURCE_URI.to_string(), @@ -215,6 +259,75 @@ fn apply_session_action_updates_only_the_target_session() { ); } +#[test] +fn automation_snapshots_and_actions_are_mirrored() { + let host = HostId::new("alpha"); + let mut mirror = MultiHostStateMirror::new(); + mirror.apply_snapshot(&host, &automation_snapshot()); + mirror.apply_snapshot(&host, &automation_run_snapshot()); + + let definition_changed = serde_json::from_value(serde_json::json!({ + "type": "automation/definitionChanged", + "definition": { + "title": "New", + "message": { "text": "triage", "origin": { "kind": "user" } }, + "session": {}, + "enabled": false, + "triggers": [] + }, + "revision": 2, + "modifiedAt": "2026-08-05T13:00:00Z" + })) + .expect("definitionChanged action"); + mirror.apply_envelope( + &host, + &ActionEnvelope { + channel: "ahp-automation:/a1".into(), + action: definition_changed, + server_seq: 3, + origin: None, + rejection_reason: None, + }, + ); + + let session_set = serde_json::from_value(serde_json::json!({ + "type": "automationRun/sessionSet", + "session": "ahp-session:/s1" + })) + .expect("sessionSet action"); + mirror.apply_envelope( + &host, + &ActionEnvelope { + channel: "ahp-automation-run:/r1".into(), + action: session_set, + server_seq: 4, + origin: None, + rejection_reason: None, + }, + ); + + let automation_key = HostedResourceKey::new(host.clone(), "ahp-automation:/a1"); + let run_key = HostedResourceKey::new(host.clone(), "ahp-automation-run:/r1"); + assert_eq!( + mirror + .automations() + .get(&automation_key) + .map(|state| (state.definition.title.as_str(), state.revision)), + Some(("New", 2)) + ); + assert_eq!( + mirror + .automation_runs() + .get(&run_key) + .map(|state| state.sessions.clone()), + Some(vec!["ahp-session:/s1".to_string()]) + ); + + mirror.reset_host(&host); + assert!(mirror.automations().is_empty()); + assert!(mirror.automation_runs().is_empty()); +} + #[test] fn apply_host_subscription_event_forwards_to_per_host_apply() { let mut mirror = MultiHostStateMirror::new(); @@ -351,6 +464,7 @@ fn non_action_event_is_ignored() { title: "new".into(), status: SessionStatus::Idle.bits(), activity: None, + origin: None, created_at: "1970-01-01T00:00:00.000Z".into(), modified_at: "1970-01-01T00:00:00.000Z".into(), project: None, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index da101cfd4..1524d60a5 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -91,6 +91,17 @@ public enum ActionType: String, Codable, Sendable { case terminalCommandExecuted = "terminal/commandExecuted" case terminalCommandFinished = "terminal/commandFinished" case resourceWatchChanged = "resourceWatch/changed" + case automationDefinitionChanged = "automation/definitionChanged" + case automationRunSummarySet = "automation/runSummarySet" + case automationRunSummaryRemoved = "automation/runSummaryRemoved" + case automationRunsLoaded = "automation/runsLoaded" + case automationRunLifecycleChanged = "automationRun/lifecycleChanged" + case automationRunSessionSet = "automationRun/sessionSet" + case automationRunSessionRemoved = "automationRun/sessionRemoved" + case automationRunPrimarySessionChanged = "automationRun/primarySessionChanged" + case automationRunArtifactSet = "automationRun/artifactSet" + case automationRunArtifactRemoved = "automationRun/artifactRemoved" + case automationRunCancelRequested = "automationRun/cancelRequested" } // MARK: - Action Infrastructure @@ -1967,6 +1978,161 @@ public struct ResourceWatchChangedAction: Codable, Sendable { } } +public struct AutomationDefinitionChangedAction: Codable, Sendable { + public var type: ActionType + public var definition: AutomationDefinition + public var revision: Int + public var modifiedAt: String + public var nextRunAt: String? + + public init( + type: ActionType, + definition: AutomationDefinition, + revision: Int, + modifiedAt: String, + nextRunAt: String? = nil + ) { + self.type = type + self.definition = definition + self.revision = revision + self.modifiedAt = modifiedAt + self.nextRunAt = nextRunAt + } +} + +public struct AutomationRunSummarySetAction: Codable, Sendable { + public var type: ActionType + public var run: AutomationRunSummary + + public init( + type: ActionType, + run: AutomationRunSummary + ) { + self.type = type + self.run = run + } +} + +public struct AutomationRunSummaryRemovedAction: Codable, Sendable { + public var type: ActionType + public var run: String + + public init( + type: ActionType, + run: String + ) { + self.type = type + self.run = run + } +} + +public struct AutomationRunsLoadedAction: Codable, Sendable { + public var type: ActionType + public var runs: [AutomationRunSummary] + public var nextCursor: String? + + public init( + type: ActionType, + runs: [AutomationRunSummary], + nextCursor: String? = nil + ) { + self.type = type + self.runs = runs + self.nextCursor = nextCursor + } +} + +public struct AutomationRunLifecycleChangedAction: Codable, Sendable { + public var type: ActionType + public var lifecycle: AutomationRunLifecycle + public var operations: [AutomationRunOperation] + + public init( + type: ActionType, + lifecycle: AutomationRunLifecycle, + operations: [AutomationRunOperation] + ) { + self.type = type + self.lifecycle = lifecycle + self.operations = operations + } +} + +public struct AutomationRunSessionSetAction: Codable, Sendable { + public var type: ActionType + public var session: String + + public init( + type: ActionType, + session: String + ) { + self.type = type + self.session = session + } +} + +public struct AutomationRunSessionRemovedAction: Codable, Sendable { + public var type: ActionType + public var session: String + + public init( + type: ActionType, + session: String + ) { + self.type = type + self.session = session + } +} + +public struct AutomationRunPrimarySessionChangedAction: Codable, Sendable { + public var type: ActionType + public var primarySession: String? + + public init( + type: ActionType, + primarySession: String? = nil + ) { + self.type = type + self.primarySession = primarySession + } +} + +public struct AutomationRunArtifactSetAction: Codable, Sendable { + public var type: ActionType + public var artifact: AutomationRunArtifact + + public init( + type: ActionType, + artifact: AutomationRunArtifact + ) { + self.type = type + self.artifact = artifact + } +} + +public struct AutomationRunArtifactRemovedAction: Codable, Sendable { + public var type: ActionType + public var artifactId: String + + public init( + type: ActionType, + artifactId: String + ) { + self.type = type + self.artifactId = artifactId + } +} + +public struct AutomationRunCancelRequestedAction: Codable, Sendable { + public var type: ActionType + + public init( + type: ActionType + ) { + self.type = type + } +} + // MARK: - Partial Summary Types public struct PartialChatSummary: Codable, Sendable { @@ -2102,6 +2268,17 @@ public enum StateAction: Codable, Sendable { case terminalCommandExecuted(TerminalCommandExecutedAction) case terminalCommandFinished(TerminalCommandFinishedAction) case resourceWatchChanged(ResourceWatchChangedAction) + case automationDefinitionChanged(AutomationDefinitionChangedAction) + case automationRunSummarySet(AutomationRunSummarySetAction) + case automationRunSummaryRemoved(AutomationRunSummaryRemovedAction) + case automationRunsLoaded(AutomationRunsLoadedAction) + case automationRunLifecycleChanged(AutomationRunLifecycleChangedAction) + case automationRunSessionSet(AutomationRunSessionSetAction) + case automationRunSessionRemoved(AutomationRunSessionRemovedAction) + case automationRunPrimarySessionChanged(AutomationRunPrimarySessionChangedAction) + case automationRunArtifactSet(AutomationRunArtifactSetAction) + case automationRunArtifactRemoved(AutomationRunArtifactRemovedAction) + case automationRunCancelRequested(AutomationRunCancelRequestedAction) /// Unknown or future action type; reducers treat this as a no-op. /// The raw payload (including its `type` discriminant) is preserved /// as an `AnyCodable` so a decode→encode round-trip re-emits it @@ -2284,6 +2461,28 @@ public enum StateAction: Codable, Sendable { self = .terminalCommandFinished(try TerminalCommandFinishedAction(from: decoder)) case "resourceWatch/changed": self = .resourceWatchChanged(try ResourceWatchChangedAction(from: decoder)) + case "automation/definitionChanged": + self = .automationDefinitionChanged(try AutomationDefinitionChangedAction(from: decoder)) + case "automation/runSummarySet": + self = .automationRunSummarySet(try AutomationRunSummarySetAction(from: decoder)) + case "automation/runSummaryRemoved": + self = .automationRunSummaryRemoved(try AutomationRunSummaryRemovedAction(from: decoder)) + case "automation/runsLoaded": + self = .automationRunsLoaded(try AutomationRunsLoadedAction(from: decoder)) + case "automationRun/lifecycleChanged": + self = .automationRunLifecycleChanged(try AutomationRunLifecycleChangedAction(from: decoder)) + case "automationRun/sessionSet": + self = .automationRunSessionSet(try AutomationRunSessionSetAction(from: decoder)) + case "automationRun/sessionRemoved": + self = .automationRunSessionRemoved(try AutomationRunSessionRemovedAction(from: decoder)) + case "automationRun/primarySessionChanged": + self = .automationRunPrimarySessionChanged(try AutomationRunPrimarySessionChangedAction(from: decoder)) + case "automationRun/artifactSet": + self = .automationRunArtifactSet(try AutomationRunArtifactSetAction(from: decoder)) + case "automationRun/artifactRemoved": + self = .automationRunArtifactRemoved(try AutomationRunArtifactRemovedAction(from: decoder)) + case "automationRun/cancelRequested": + self = .automationRunCancelRequested(try AutomationRunCancelRequestedAction(from: decoder)) default: self = .unknown(try AnyCodable(from: decoder)) } @@ -2376,6 +2575,17 @@ public enum StateAction: Codable, Sendable { case .terminalCommandExecuted(let v): try v.encode(to: encoder) case .terminalCommandFinished(let v): try v.encode(to: encoder) case .resourceWatchChanged(let v): try v.encode(to: encoder) + case .automationDefinitionChanged(let v): try v.encode(to: encoder) + case .automationRunSummarySet(let v): try v.encode(to: encoder) + case .automationRunSummaryRemoved(let v): try v.encode(to: encoder) + case .automationRunsLoaded(let v): try v.encode(to: encoder) + case .automationRunLifecycleChanged(let v): try v.encode(to: encoder) + case .automationRunSessionSet(let v): try v.encode(to: encoder) + case .automationRunSessionRemoved(let v): try v.encode(to: encoder) + case .automationRunPrimarySessionChanged(let v): try v.encode(to: encoder) + case .automationRunArtifactSet(let v): try v.encode(to: encoder) + case .automationRunArtifactRemoved(let v): try v.encode(to: encoder) + case .automationRunCancelRequested(let v): try v.encode(to: encoder) case .unknown(let value): try value.encode(to: encoder) } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 4530778ed..f670b2eed 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -266,6 +266,8 @@ public struct InitializeResult: Codable, Sendable { /// defines a template variable, `{level}`, for subscriber-side severity /// filtering). Clients MAY ignore signals they cannot process. public var telemetry: TelemetryCapabilities? + /// Host automation support. Absence means unsupported. + public var automations: AutomationCapabilities? public init( protocolVersion: String, @@ -275,7 +277,8 @@ public struct InitializeResult: Codable, Sendable { defaultDirectory: String? = nil, completionTriggerCharacters: [String]? = nil, terminalCommandPrefix: String? = nil, - telemetry: TelemetryCapabilities? = nil + telemetry: TelemetryCapabilities? = nil, + automations: AutomationCapabilities? = nil ) { self.protocolVersion = protocolVersion self.serverSeq = serverSeq @@ -285,6 +288,7 @@ public struct InitializeResult: Codable, Sendable { self.completionTriggerCharacters = completionTriggerCharacters self.terminalCommandPrefix = terminalCommandPrefix self.telemetry = telemetry + self.automations = automations } } @@ -309,6 +313,91 @@ public struct ClientCapabilities: Codable, Sendable { } } +public struct AutomationCapabilities: Codable, Sendable { + public var execution: AutomationExecutionCapabilities + public var create: AutomationCreateCapability? + public var schedules: AutomationScheduleCapabilities? + public var runCancellation: AutomationRunCancellationCapability? + public var schedulePreview: AutomationSchedulePreviewCapability? + public var runHistoryLimit: Int? + + public init( + execution: AutomationExecutionCapabilities, + create: AutomationCreateCapability? = nil, + schedules: AutomationScheduleCapabilities? = nil, + runCancellation: AutomationRunCancellationCapability? = nil, + schedulePreview: AutomationSchedulePreviewCapability? = nil, + runHistoryLimit: Int? = nil + ) { + self.execution = execution + self.create = create + self.schedules = schedules + self.runCancellation = runCancellation + self.schedulePreview = schedulePreview + self.runHistoryLimit = runHistoryLimit + } +} + +public struct AutomationExecutionCapabilities: Codable, Sendable { + public var lifetime: AutomationExecutionLifetime + + public init( + lifetime: AutomationExecutionLifetime + ) { + self.lifetime = lifetime + } +} + +public struct AutomationCreateCapability: Codable, Sendable { + + public init( + + ) { + } +} + +public struct AutomationScheduleCapabilities: Codable, Sendable { + public var kinds: [AutomationScheduleKind] + public var cron: AutomationCronScheduleCapability? + + public init( + kinds: [AutomationScheduleKind], + cron: AutomationCronScheduleCapability? = nil + ) { + self.kinds = kinds + self.cron = cron + } +} + +public struct AutomationCronScheduleCapability: Codable, Sendable { + public var dialect: String + public var minIntervalMinutes: Int? + + public init( + dialect: String, + minIntervalMinutes: Int? = nil + ) { + self.dialect = dialect + self.minIntervalMinutes = minIntervalMinutes + } +} + +public struct AutomationRunCancellationCapability: Codable, Sendable { + + public init( + + ) { + } +} + +public struct AutomationSchedulePreviewCapability: Codable, Sendable { + + public init( + + ) { + } +} + public struct Implementation: Codable, Sendable { /// Implementation name, e.g. a product or package identifier. public var name: String @@ -1820,6 +1909,237 @@ public struct ChangesetOperationFollowUp: Codable, Sendable { } } +public struct ListAutomationsParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + /// Maximum number of entries to return in this page. The server SHOULD respect + /// this bound but MAY return fewer entries and MAY impose its own upper cap. + /// Omit to let the server choose the page size. + public var limit: Int? + /// Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}. + /// Omit to fetch the first page. Cursors are server-defined and MUST be treated + /// as opaque — do not parse, modify, or persist them across connections. An + /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. + public var cursor: String? + public var enabled: Bool? + + public init( + channel: String, + limit: Int? = nil, + cursor: String? = nil, + enabled: Bool? = nil + ) { + self.channel = channel + self.limit = limit + self.cursor = cursor + self.enabled = enabled + } +} + +public struct ListAutomationsResult: Codable, Sendable { + /// Opaque cursor for the next page. Present when more entries exist beyond the + /// returned page; absent signals the end of the collection. Pass it back as + /// {@link PaginatedParams.cursor} to fetch the following page. + public var nextCursor: String? + public var items: [AutomationSummary] + + public init( + nextCursor: String? = nil, + items: [AutomationSummary] + ) { + self.nextCursor = nextCursor + self.items = items + } +} + +public struct ListAutomationTriggerDefinitionsParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + public var provider: String? + public var workingDirectories: [String]? + public var sessionConfig: [String: AnyCodable]? + + public init( + channel: String, + provider: String? = nil, + workingDirectories: [String]? = nil, + sessionConfig: [String: AnyCodable]? = nil + ) { + self.channel = channel + self.provider = provider + self.workingDirectories = workingDirectories + self.sessionConfig = sessionConfig + } +} + +public struct ListAutomationTriggerDefinitionsResult: Codable, Sendable { + public var items: [AutomationTriggerDefinition] + + public init( + items: [AutomationTriggerDefinition] + ) { + self.items = items + } +} + +public struct CreateAutomationParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + public var definition: AutomationDefinition + public var `import`: AnyCodable? + + enum CodingKeys: String, CodingKey { + case channel + case definition + case `import` = "import" + } + + public init( + channel: String, + definition: AutomationDefinition, + `import`: AnyCodable? = nil + ) { + self.channel = channel + self.definition = definition + self.`import` = `import` + } +} + +public struct AutomationDefinitionPatch: Codable, Sendable { + public var title: String? + public var message: Message? + public var session: AutomationSessionTemplate? + public var enabled: Bool? + public var triggers: [AutomationTrigger]? + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case title + case message + case session + case enabled + case triggers + case meta = "_meta" + } + + public init( + title: String? = nil, + message: Message? = nil, + session: AutomationSessionTemplate? = nil, + enabled: Bool? = nil, + triggers: [AutomationTrigger]? = nil, + meta: [String: AnyCodable]? = nil + ) { + self.title = title + self.message = message + self.session = session + self.enabled = enabled + self.triggers = triggers + self.meta = meta + } +} + +public struct UpdateAutomationParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + public var expectedRevision: Int + public var changes: AutomationDefinitionPatch + + public init( + channel: String, + expectedRevision: Int, + changes: AutomationDefinitionPatch + ) { + self.channel = channel + self.expectedRevision = expectedRevision + self.changes = changes + } +} + +public struct DisposeAutomationParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + + public init( + channel: String + ) { + self.channel = channel + } +} + +public struct RunAutomationParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + public var requestId: String + + public init( + channel: String, + requestId: String + ) { + self.channel = channel + self.requestId = requestId + } +} + +public struct RunAutomationResult: Codable, Sendable { + public var run: String + + public init( + run: String + ) { + self.run = run + } +} + +public struct FetchAutomationRunsParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + public var cursor: String? + + public init( + channel: String, + cursor: String? = nil + ) { + self.channel = channel + self.cursor = cursor + } +} + +public struct FetchAutomationRunsResult: Codable, Sendable { + + public init( + + ) { + } +} + +public struct PreviewAutomationScheduleParams: Codable, Sendable { + /// Channel URI this command targets. + public var channel: String + public var schedule: AutomationSchedule + public var count: Int? + + public init( + channel: String, + schedule: AutomationSchedule, + count: Int? = nil + ) { + self.channel = channel + self.schedule = schedule + self.count = count + } +} + +public struct PreviewAutomationScheduleResult: Codable, Sendable { + public var items: [String] + + public init( + items: [String] + ) { + self.items = items + } +} + // MARK: - Command Unions public enum ChatSource: Codable, Sendable { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 4ffbb95c7..eadc50f8d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -66,6 +66,45 @@ public struct SessionSummaryChangedParams: Codable, Sendable { } } +public struct AutomationAddedParams: Codable, Sendable { + public var channel: String + public var summary: AutomationSummary + + public init( + channel: String, + summary: AutomationSummary + ) { + self.channel = channel + self.summary = summary + } +} + +public struct AutomationRemovedParams: Codable, Sendable { + public var channel: String + public var automation: String + + public init( + channel: String, + automation: String + ) { + self.channel = channel + self.automation = automation + } +} + +public struct AutomationSummaryChangedParams: Codable, Sendable { + public var channel: String + public var summary: AutomationSummary + + public init( + channel: String, + summary: AutomationSummary + ) { + self.channel = channel + self.summary = summary + } +} + public struct ProgressParams: Codable, Sendable { /// Channel URI this notification belongs to (the root channel). public var channel: String @@ -180,6 +219,8 @@ public struct PartialSessionSummary: Codable, Sendable { public var status: SessionStatus? /// Human-readable description of what the session is currently doing public var activity: String? + /// Durable origin of this session, when another AHP resource created it. + public var origin: SessionOrigin? /// Server-owned project for this session public var project: ProjectInfo? /// The working directories the session's agent has tool access to, as @@ -219,6 +260,7 @@ public struct PartialSessionSummary: Codable, Sendable { case title case status case activity + case origin case project case workingDirectories case annotations @@ -234,6 +276,7 @@ public struct PartialSessionSummary: Codable, Sendable { title: String? = nil, status: SessionStatus? = nil, activity: String? = nil, + origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, @@ -247,6 +290,7 @@ public struct PartialSessionSummary: Codable, Sendable { self.title = title self.status = status self.activity = activity + self.origin = origin self.project = project self.workingDirectories = workingDirectories self.annotations = annotations diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 7ec7dd9b2..a30964e79 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -400,6 +400,73 @@ public enum ResourceChangeType: String, Codable, Sendable { case deleted = "deleted" } +public enum SessionOriginKind: String, Codable, Sendable { + case automation = "automation" +} + +public enum AutomationOperation: String, Codable, Sendable { + case update = "update" + case dispose = "dispose" + case run = "run" +} + +public enum AutomationExecutionLifetime: String, Codable, Sendable { + case hostLifetime = "hostLifetime" + case managed = "managed" +} + +public enum AutomationScheduleKind: String, Codable, Sendable { + case hourly = "hourly" + case daily = "daily" + case weekly = "weekly" + case cron = "cron" +} + +public enum AutomationWeekday: String, Codable, Sendable { + case monday = "monday" + case tuesday = "tuesday" + case wednesday = "wednesday" + case thursday = "thursday" + case friday = "friday" + case saturday = "saturday" + case sunday = "sunday" +} + +public enum AutomationMisfirePolicy: String, Codable, Sendable { + case skip = "skip" + case runOnce = "runOnce" +} + +public enum AutomationTriggerKind: String, Codable, Sendable { + case schedule = "schedule" + case event = "event" +} + +public enum AutomationRunStatus: String, Codable, Sendable { + case pending = "pending" + case running = "running" + case blocked = "blocked" + case completed = "completed" + case failed = "failed" + case cancelled = "cancelled" +} + +public enum AutomationRunBlockerKind: String, Codable, Sendable { + case userInput = "userInput" + case toolConfirmation = "toolConfirmation" + case authentication = "authentication" + case clientExecution = "clientExecution" +} + +public enum AutomationRunCauseKind: String, Codable, Sendable { + case manual = "manual" + case trigger = "trigger" +} + +public enum AutomationRunOperation: String, Codable, Sendable { + case cancel = "cancel" +} + // MARK: - State Types public struct Icon: Codable, Sendable { @@ -1077,6 +1144,8 @@ public struct SessionState: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the session is currently doing public var activity: String? + /// Durable origin of this session, when another AHP resource created it. + public var origin: SessionOrigin? /// Server-owned project for this session public var project: ProjectInfo? /// The working directories the session's agent has tool access to, as @@ -1175,6 +1244,7 @@ public struct SessionState: Codable, Sendable { case title case status case activity + case origin case project case workingDirectories case annotations @@ -1196,6 +1266,7 @@ public struct SessionState: Codable, Sendable { title: String, status: SessionStatus, activity: String? = nil, + origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, @@ -1215,6 +1286,7 @@ public struct SessionState: Codable, Sendable { self.title = title self.status = status self.activity = activity + self.origin = origin self.project = project self.workingDirectories = workingDirectories self.annotations = annotations @@ -1400,6 +1472,8 @@ public struct SessionSummary: Codable, Sendable { public var status: SessionStatus /// Human-readable description of what the session is currently doing public var activity: String? + /// Durable origin of this session, when another AHP resource created it. + public var origin: SessionOrigin? /// Server-owned project for this session public var project: ProjectInfo? /// The working directories the session's agent has tool access to, as @@ -1439,6 +1513,7 @@ public struct SessionSummary: Codable, Sendable { case title case status case activity + case origin case project case workingDirectories case annotations @@ -1454,6 +1529,7 @@ public struct SessionSummary: Codable, Sendable { title: String, status: SessionStatus, activity: String? = nil, + origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, annotations: AnnotationsSummary? = nil, @@ -1467,6 +1543,7 @@ public struct SessionSummary: Codable, Sendable { self.title = title self.status = status self.activity = activity + self.origin = origin self.project = project self.workingDirectories = workingDirectories self.annotations = annotations @@ -5227,173 +5304,821 @@ public struct ResourceChange: Codable, Sendable { } } -// MARK: - Tool Input - -/// Raw tool input represented inline or by content reference. -public enum ToolInput: Codable, Sendable { - case inline(String) - case contentRef(ContentRef) +public struct AutomationSessionOrigin: Codable, Sendable { + public var kind: SessionOriginKind + public var automation: String + public var run: String - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let value = try? container.decode(String.self) { - self = .inline(value) - } else { - self = .contentRef(try container.decode(ContentRef.self)) - } + public init( + kind: SessionOriginKind, + automation: String, + run: String + ) { + self.kind = kind + self.automation = automation + self.run = run } +} - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .inline(let value): try container.encode(value) - case .contentRef(let value): try container.encode(value) - } +public struct AutomationLocalTime: Codable, Sendable { + public var hour: Int + public var minute: Int + + public init( + hour: Int, + minute: Int + ) { + self.hour = hour + self.minute = minute } } -// MARK: - Discriminated Unions +public struct AutomationHourlySchedule: Codable, Sendable { + public var kind: AutomationScheduleKind -public struct ChatOriginUser: Codable, Sendable { - public var kind: ChatOriginKind + public init( + kind: AutomationScheduleKind + ) { + self.kind = kind + } +} - public init(kind: ChatOriginKind = .user) { +public struct AutomationDailySchedule: Codable, Sendable { + public var kind: AutomationScheduleKind + public var time: AutomationLocalTime + /// IANA time-zone identifier. + public var timeZone: String + + public init( + kind: AutomationScheduleKind, + time: AutomationLocalTime, + timeZone: String + ) { self.kind = kind + self.time = time + self.timeZone = timeZone } } -public struct ChatOriginFork: Codable, Sendable { - public var kind: ChatOriginKind - public var chat: String - public var turnId: String +public struct AutomationWeeklySchedule: Codable, Sendable { + public var kind: AutomationScheduleKind + public var weekday: AutomationWeekday + public var time: AutomationLocalTime + /// IANA time-zone identifier. + public var timeZone: String - public init(kind: ChatOriginKind = .fork, chat: String, turnId: String) { + public init( + kind: AutomationScheduleKind, + weekday: AutomationWeekday, + time: AutomationLocalTime, + timeZone: String + ) { self.kind = kind - self.chat = chat - self.turnId = turnId + self.weekday = weekday + self.time = time + self.timeZone = timeZone } } -public struct ChatOriginTool: Codable, Sendable { - public var kind: ChatOriginKind - public var chat: String - public var toolCallId: String +public struct AutomationCronSchedule: Codable, Sendable { + public var kind: AutomationScheduleKind + /// Standard five-field Unix cron expression. + public var expression: String + /// IANA time-zone identifier. + public var timeZone: String - public init(kind: ChatOriginKind = .tool, chat: String, toolCallId: String) { + public init( + kind: AutomationScheduleKind, + expression: String, + timeZone: String + ) { self.kind = kind - self.chat = chat - self.toolCallId = toolCallId + self.expression = expression + self.timeZone = timeZone } } -public struct ChatOriginSideChat: Codable, Sendable { - public var kind: ChatOriginKind - public var chat: String - public var turnId: String - public var selection: SideChatSelection? +public struct AutomationScheduleTrigger: Codable, Sendable { + /// Stable within the automation definition. + public var id: String + public var kind: AutomationTriggerKind + public var schedule: AutomationSchedule + public var misfirePolicy: AutomationMisfirePolicy? - public init(kind: ChatOriginKind = .sideChat, chat: String, turnId: String, selection: SideChatSelection? = nil) { + public init( + id: String, + kind: AutomationTriggerKind, + schedule: AutomationSchedule, + misfirePolicy: AutomationMisfirePolicy? = nil + ) { + self.id = id self.kind = kind - self.chat = chat - self.turnId = turnId - self.selection = selection + self.schedule = schedule + self.misfirePolicy = misfirePolicy } } -public enum ChatOrigin: Codable, Sendable { - case user(ChatOriginUser) - case fork(ChatOriginFork) - case sideChat(ChatOriginSideChat) - case tool(ChatOriginTool) +public struct AutomationEventTrigger: Codable, Sendable { + /// Stable within the automation definition. + public var id: String + public var kind: AutomationTriggerKind + /// Stable host-defined trigger type. + public var type: String + /// Selected event actions. + public var events: [String] + /// Schema-defined values. Unknown entries must survive round-trips. + public var config: [String: AnyCodable]? - private enum DiscriminatorCodingKeys: String, CodingKey { case kind } + public init( + id: String, + kind: AutomationTriggerKind, + type: String, + events: [String], + config: [String: AnyCodable]? = nil + ) { + self.id = id + self.kind = kind + self.type = type + self.events = events + self.config = config + } +} - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: DiscriminatorCodingKeys.self) - let discriminant = try container.decode(String.self, forKey: .kind) - switch discriminant { - case "user": self = .user(try ChatOriginUser(from: decoder)) - case "fork": self = .fork(try ChatOriginFork(from: decoder)) - case "sideChat": self = .sideChat(try ChatOriginSideChat(from: decoder)) - case "tool": self = .tool(try ChatOriginTool(from: decoder)) - default: - throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown ChatOrigin kind: \(discriminant)") - } +public struct AutomationTriggerEventDefinition: Codable, Sendable { + public var id: String + public var title: String + public var description: String? + + public init( + id: String, + title: String, + description: String? = nil + ) { + self.id = id + self.title = title + self.description = description } +} - public func encode(to encoder: Encoder) throws { - switch self { - case .user(let value): try value.encode(to: encoder) - case .fork(let value): try value.encode(to: encoder) - case .sideChat(let value): try value.encode(to: encoder) - case .tool(let value): try value.encode(to: encoder) - } +public struct AutomationTriggerDefinition: Codable, Sendable { + public var type: String + public var title: String + public var description: String? + public var events: [AutomationTriggerEventDefinition] + public var configSchema: ConfigSchema? + + public init( + type: String, + title: String, + description: String? = nil, + events: [AutomationTriggerEventDefinition], + configSchema: ConfigSchema? = nil + ) { + self.type = type + self.title = title + self.description = description + self.events = events + self.configSchema = configSchema } } -public enum ResponsePart: Codable, Sendable { - case markdown(MarkdownResponsePart) - case contentRef(ResourceResponsePart) - case toolCall(ToolCallResponsePart) - case reasoning(ReasoningResponsePart) - case systemNotification(SystemNotificationResponsePart) - case inputRequest(InputRequestResponsePart) - /// Unknown or future discriminant; the raw payload is preserved - /// and re-encoded verbatim for forward-compatibility. - case unknown(AnyCodable) +public struct AutomationSessionTemplate: Codable, Sendable { + public var provider: String? + /// Absence means a workspace-less session. + public var workingDirectories: [String]? + /// Values resolved through `resolveSessionConfig`. + public var config: [String: AnyCodable]? - private enum DiscriminantKey: String, CodingKey { - case discriminant = "kind" + public init( + provider: String? = nil, + workingDirectories: [String]? = nil, + config: [String: AnyCodable]? = nil + ) { + self.provider = provider + self.workingDirectories = workingDirectories + self.config = config } +} - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: DiscriminantKey.self) - let discriminant = try container.decode(String.self, forKey: .discriminant) - switch discriminant { - case "markdown": - self = .markdown(try MarkdownResponsePart(from: decoder)) - case "contentRef": - self = .contentRef(try ResourceResponsePart(from: decoder)) - case "toolCall": - self = .toolCall(try ToolCallResponsePart(from: decoder)) - case "reasoning": - self = .reasoning(try ReasoningResponsePart(from: decoder)) - case "systemNotification": - self = .systemNotification(try SystemNotificationResponsePart(from: decoder)) - case "inputRequest": - self = .inputRequest(try InputRequestResponsePart(from: decoder)) - default: - self = .unknown(try AnyCodable(from: decoder)) - } +public struct AutomationDefinition: Codable, Sendable { + public var title: String + /// Initial user message sent to each new session. + public var message: Message + public var session: AutomationSessionTemplate + /// Controls automatic triggers; manual runs remain permitted. + public var enabled: Bool + /// Empty means manual-only. + public var triggers: [AutomationTrigger] + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case title + case message + case session + case enabled + case triggers + case meta = "_meta" } - public func encode(to encoder: Encoder) throws { - switch self { - case .markdown(let value): try value.encode(to: encoder) - case .contentRef(let value): try value.encode(to: encoder) - case .toolCall(let value): try value.encode(to: encoder) - case .reasoning(let value): try value.encode(to: encoder) - case .systemNotification(let value): try value.encode(to: encoder) - case .inputRequest(let value): try value.encode(to: encoder) - case .unknown(let value): try value.encode(to: encoder) - } + public init( + title: String, + message: Message, + session: AutomationSessionTemplate, + enabled: Bool, + triggers: [AutomationTrigger], + meta: [String: AnyCodable]? = nil + ) { + self.title = title + self.message = message + self.session = session + self.enabled = enabled + self.triggers = triggers + self.meta = meta } } -public enum ToolCallState: Codable, Sendable { - case streaming(ToolCallStreamingState) - case pendingConfirmation(ToolCallPendingConfirmationState) - case running(ToolCallRunningState) - case authRequired(ToolCallAuthRequiredState) - case pendingResultConfirmation(ToolCallPendingResultConfirmationState) - case completed(ToolCallCompletedState) - case cancelled(ToolCallCancelledState) - /// Unknown or future discriminant; the raw payload is preserved - /// and re-encoded verbatim for forward-compatibility. - case unknown(AnyCodable) +public struct AutomationRuntimeState: Codable, Sendable { + public var workingDirectories: [String]? + public var meta: [String: AnyCodable]? - private enum DiscriminantKey: String, CodingKey { - case discriminant = "status" + enum CodingKeys: String, CodingKey { + case workingDirectories + case meta = "_meta" + } + + public init( + workingDirectories: [String]? = nil, + meta: [String: AnyCodable]? = nil + ) { + self.workingDirectories = workingDirectories + self.meta = meta + } +} + +public struct AutomationSummary: Codable, Sendable { + public var resource: String + public var title: String + public var enabled: Bool + public var triggerCount: Int + public var nextRunAt: String? + public var lastRun: AutomationRunSummary? + public var revision: Int + public var operations: [AutomationOperation] + public var createdAt: String + public var modifiedAt: String + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case resource + case title + case enabled + case triggerCount + case nextRunAt + case lastRun + case revision + case operations + case createdAt + case modifiedAt + case meta = "_meta" + } + + public init( + resource: String, + title: String, + enabled: Bool, + triggerCount: Int, + nextRunAt: String? = nil, + lastRun: AutomationRunSummary? = nil, + revision: Int, + operations: [AutomationOperation], + createdAt: String, + modifiedAt: String, + meta: [String: AnyCodable]? = nil + ) { + self.resource = resource + self.title = title + self.enabled = enabled + self.triggerCount = triggerCount + self.nextRunAt = nextRunAt + self.lastRun = lastRun + self.revision = revision + self.operations = operations + self.createdAt = createdAt + self.modifiedAt = modifiedAt + self.meta = meta + } +} + +public struct AutomationState: Codable, Sendable { + public var resource: String + public var definition: AutomationDefinition + public var revision: Int + public var nextRunAt: String? + /// Newest-first retained run summaries. + public var runs: [AutomationRunSummary] + public var runsNextCursor: String? + public var runtime: AutomationRuntimeState? + public var operations: [AutomationOperation] + public var createdAt: String + public var modifiedAt: String + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case resource + case definition + case revision + case nextRunAt + case runs + case runsNextCursor + case runtime + case operations + case createdAt + case modifiedAt + case meta = "_meta" + } + + public init( + resource: String, + definition: AutomationDefinition, + revision: Int, + nextRunAt: String? = nil, + runs: [AutomationRunSummary], + runsNextCursor: String? = nil, + runtime: AutomationRuntimeState? = nil, + operations: [AutomationOperation], + createdAt: String, + modifiedAt: String, + meta: [String: AnyCodable]? = nil + ) { + self.resource = resource + self.definition = definition + self.revision = revision + self.nextRunAt = nextRunAt + self.runs = runs + self.runsNextCursor = runsNextCursor + self.runtime = runtime + self.operations = operations + self.createdAt = createdAt + self.modifiedAt = modifiedAt + self.meta = meta + } +} + +public struct AutomationRunBlocker: Codable, Sendable { + public var kind: AutomationRunBlockerKind + + public init( + kind: AutomationRunBlockerKind + ) { + self.kind = kind + } +} + +public struct AutomationManualRunCause: Codable, Sendable { + public var kind: AutomationRunCauseKind + + public init( + kind: AutomationRunCauseKind + ) { + self.kind = kind + } +} + +public struct AutomationTriggeredRunCause: Codable, Sendable { + public var kind: AutomationRunCauseKind + public var triggerId: String + public var scheduledFor: String? + public var catchUp: Bool? + /// Host-defined event provenance containing no secrets. + public var event: [String: AnyCodable]? + + public init( + kind: AutomationRunCauseKind, + triggerId: String, + scheduledFor: String? = nil, + catchUp: Bool? = nil, + event: [String: AnyCodable]? = nil + ) { + self.kind = kind + self.triggerId = triggerId + self.scheduledFor = scheduledFor + self.catchUp = catchUp + self.event = event + } +} + +public struct AutomationPendingRunLifecycle: Codable, Sendable { + public var status: AutomationRunStatus + public var createdAt: String + + public init( + status: AutomationRunStatus, + createdAt: String + ) { + self.status = status + self.createdAt = createdAt + } +} + +public struct AutomationRunningRunLifecycle: Codable, Sendable { + public var status: AutomationRunStatus + public var createdAt: String + public var startedAt: String + + public init( + status: AutomationRunStatus, + createdAt: String, + startedAt: String + ) { + self.status = status + self.createdAt = createdAt + self.startedAt = startedAt + } +} + +public struct AutomationBlockedRunLifecycle: Codable, Sendable { + public var status: AutomationRunStatus + public var createdAt: String + public var startedAt: String + public var blocker: AutomationRunBlocker + + public init( + status: AutomationRunStatus, + createdAt: String, + startedAt: String, + blocker: AutomationRunBlocker + ) { + self.status = status + self.createdAt = createdAt + self.startedAt = startedAt + self.blocker = blocker + } +} + +public struct AutomationCompletedRunLifecycle: Codable, Sendable { + public var status: AutomationRunStatus + public var createdAt: String + public var startedAt: String + public var completedAt: String + public var usage: UsageInfo? + + public init( + status: AutomationRunStatus, + createdAt: String, + startedAt: String, + completedAt: String, + usage: UsageInfo? = nil + ) { + self.status = status + self.createdAt = createdAt + self.startedAt = startedAt + self.completedAt = completedAt + self.usage = usage + } +} + +public struct AutomationFailedRunLifecycle: Codable, Sendable { + public var status: AutomationRunStatus + public var createdAt: String + public var startedAt: String? + public var completedAt: String + public var error: ErrorInfo + + public init( + status: AutomationRunStatus, + createdAt: String, + startedAt: String? = nil, + completedAt: String, + error: ErrorInfo + ) { + self.status = status + self.createdAt = createdAt + self.startedAt = startedAt + self.completedAt = completedAt + self.error = error + } +} + +public struct AutomationCancelledRunLifecycle: Codable, Sendable { + public var status: AutomationRunStatus + public var createdAt: String + public var startedAt: String? + public var completedAt: String + + public init( + status: AutomationRunStatus, + createdAt: String, + startedAt: String? = nil, + completedAt: String + ) { + self.status = status + self.createdAt = createdAt + self.startedAt = startedAt + self.completedAt = completedAt + } +} + +public struct AutomationRunArtifact: Codable, Sendable { + /// Content URI + public var uri: String + /// Approximate size in bytes + public var sizeHint: Int? + /// Content MIME type + public var contentType: String? + /// Content nonce + public var nonce: String? + public var id: String + public var label: String + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case uri + case sizeHint + case contentType + case nonce + case id + case label + case meta = "_meta" + } + + public init( + uri: String, + sizeHint: Int? = nil, + contentType: String? = nil, + nonce: String? = nil, + id: String, + label: String, + meta: [String: AnyCodable]? = nil + ) { + self.uri = uri + self.sizeHint = sizeHint + self.contentType = contentType + self.nonce = nonce + self.id = id + self.label = label + self.meta = meta + } +} + +public struct AutomationRunSummary: Codable, Sendable { + public var resource: String + public var automation: String + public var cause: AutomationRunCause + public var lifecycle: AutomationRunLifecycle + public var primarySession: String? + public var sessionCount: Int + public var artifactCount: Int? + public var operations: [AutomationRunOperation] + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case resource + case automation + case cause + case lifecycle + case primarySession + case sessionCount + case artifactCount + case operations + case meta = "_meta" + } + + public init( + resource: String, + automation: String, + cause: AutomationRunCause, + lifecycle: AutomationRunLifecycle, + primarySession: String? = nil, + sessionCount: Int, + artifactCount: Int? = nil, + operations: [AutomationRunOperation], + meta: [String: AnyCodable]? = nil + ) { + self.resource = resource + self.automation = automation + self.cause = cause + self.lifecycle = lifecycle + self.primarySession = primarySession + self.sessionCount = sessionCount + self.artifactCount = artifactCount + self.operations = operations + self.meta = meta + } +} + +public struct AutomationRunState: Codable, Sendable { + public var resource: String + public var automation: String + public var cause: AutomationRunCause + public var lifecycle: AutomationRunLifecycle + public var sessions: [String] + public var primarySession: String? + public var artifacts: [AutomationRunArtifact] + public var operations: [AutomationRunOperation] + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case resource + case automation + case cause + case lifecycle + case sessions + case primarySession + case artifacts + case operations + case meta = "_meta" + } + + public init( + resource: String, + automation: String, + cause: AutomationRunCause, + lifecycle: AutomationRunLifecycle, + sessions: [String], + primarySession: String? = nil, + artifacts: [AutomationRunArtifact], + operations: [AutomationRunOperation], + meta: [String: AnyCodable]? = nil + ) { + self.resource = resource + self.automation = automation + self.cause = cause + self.lifecycle = lifecycle + self.sessions = sessions + self.primarySession = primarySession + self.artifacts = artifacts + self.operations = operations + self.meta = meta + } +} + +// MARK: - Tool Input + +/// Raw tool input represented inline or by content reference. +public enum ToolInput: Codable, Sendable { + case inline(String) + case contentRef(ContentRef) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let value = try? container.decode(String.self) { + self = .inline(value) + } else { + self = .contentRef(try container.decode(ContentRef.self)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .inline(let value): try container.encode(value) + case .contentRef(let value): try container.encode(value) + } + } +} + +// MARK: - Discriminated Unions + +public struct ChatOriginUser: Codable, Sendable { + public var kind: ChatOriginKind + + public init(kind: ChatOriginKind = .user) { + self.kind = kind + } +} + +public struct ChatOriginFork: Codable, Sendable { + public var kind: ChatOriginKind + public var chat: String + public var turnId: String + + public init(kind: ChatOriginKind = .fork, chat: String, turnId: String) { + self.kind = kind + self.chat = chat + self.turnId = turnId + } +} + +public struct ChatOriginTool: Codable, Sendable { + public var kind: ChatOriginKind + public var chat: String + public var toolCallId: String + + public init(kind: ChatOriginKind = .tool, chat: String, toolCallId: String) { + self.kind = kind + self.chat = chat + self.toolCallId = toolCallId + } +} + +public struct ChatOriginSideChat: Codable, Sendable { + public var kind: ChatOriginKind + public var chat: String + public var turnId: String + public var selection: SideChatSelection? + + public init(kind: ChatOriginKind = .sideChat, chat: String, turnId: String, selection: SideChatSelection? = nil) { + self.kind = kind + self.chat = chat + self.turnId = turnId + self.selection = selection + } +} + +public enum ChatOrigin: Codable, Sendable { + case user(ChatOriginUser) + case fork(ChatOriginFork) + case sideChat(ChatOriginSideChat) + case tool(ChatOriginTool) + + private enum DiscriminatorCodingKeys: String, CodingKey { case kind } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminatorCodingKeys.self) + let discriminant = try container.decode(String.self, forKey: .kind) + switch discriminant { + case "user": self = .user(try ChatOriginUser(from: decoder)) + case "fork": self = .fork(try ChatOriginFork(from: decoder)) + case "sideChat": self = .sideChat(try ChatOriginSideChat(from: decoder)) + case "tool": self = .tool(try ChatOriginTool(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown ChatOrigin kind: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .user(let value): try value.encode(to: encoder) + case .fork(let value): try value.encode(to: encoder) + case .sideChat(let value): try value.encode(to: encoder) + case .tool(let value): try value.encode(to: encoder) + } + } +} + +public enum ResponsePart: Codable, Sendable { + case markdown(MarkdownResponsePart) + case contentRef(ResourceResponsePart) + case toolCall(ToolCallResponsePart) + case reasoning(ReasoningResponsePart) + case systemNotification(SystemNotificationResponsePart) + case inputRequest(InputRequestResponsePart) + /// Unknown or future discriminant; the raw payload is preserved + /// and re-encoded verbatim for forward-compatibility. + case unknown(AnyCodable) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "markdown": + self = .markdown(try MarkdownResponsePart(from: decoder)) + case "contentRef": + self = .contentRef(try ResourceResponsePart(from: decoder)) + case "toolCall": + self = .toolCall(try ToolCallResponsePart(from: decoder)) + case "reasoning": + self = .reasoning(try ReasoningResponsePart(from: decoder)) + case "systemNotification": + self = .systemNotification(try SystemNotificationResponsePart(from: decoder)) + case "inputRequest": + self = .inputRequest(try InputRequestResponsePart(from: decoder)) + default: + self = .unknown(try AnyCodable(from: decoder)) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .markdown(let value): try value.encode(to: encoder) + case .contentRef(let value): try value.encode(to: encoder) + case .toolCall(let value): try value.encode(to: encoder) + case .reasoning(let value): try value.encode(to: encoder) + case .systemNotification(let value): try value.encode(to: encoder) + case .inputRequest(let value): try value.encode(to: encoder) + case .unknown(let value): try value.encode(to: encoder) + } + } +} + +public enum ToolCallState: Codable, Sendable { + case streaming(ToolCallStreamingState) + case pendingConfirmation(ToolCallPendingConfirmationState) + case running(ToolCallRunningState) + case authRequired(ToolCallAuthRequiredState) + case pendingResultConfirmation(ToolCallPendingResultConfirmationState) + case completed(ToolCallCompletedState) + case cancelled(ToolCallCancelledState) + /// Unknown or future discriminant; the raw payload is preserved + /// and re-encoded verbatim for forward-compatibility. + case unknown(AnyCodable) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "status" } public init(from decoder: Decoder) throws { @@ -5979,6 +6704,201 @@ public enum SessionInputRequest: Codable, Sendable { } } +public enum SessionOrigin: Codable, Sendable { + case automation(AutomationSessionOrigin) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "automation": + self = .automation(try AutomationSessionOrigin(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown SessionOrigin discriminant: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .automation(var value): + value.kind = .automation + try value.encode(to: encoder) + } + } +} + +public enum AutomationSchedule: Codable, Sendable { + case hourly(AutomationHourlySchedule) + case daily(AutomationDailySchedule) + case weekly(AutomationWeeklySchedule) + case cron(AutomationCronSchedule) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "hourly": + self = .hourly(try AutomationHourlySchedule(from: decoder)) + case "daily": + self = .daily(try AutomationDailySchedule(from: decoder)) + case "weekly": + self = .weekly(try AutomationWeeklySchedule(from: decoder)) + case "cron": + self = .cron(try AutomationCronSchedule(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown AutomationSchedule discriminant: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .hourly(var value): + value.kind = .hourly + try value.encode(to: encoder) + case .daily(var value): + value.kind = .daily + try value.encode(to: encoder) + case .weekly(var value): + value.kind = .weekly + try value.encode(to: encoder) + case .cron(var value): + value.kind = .cron + try value.encode(to: encoder) + } + } +} + +public enum AutomationTrigger: Codable, Sendable { + case schedule(AutomationScheduleTrigger) + case event(AutomationEventTrigger) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "schedule": + self = .schedule(try AutomationScheduleTrigger(from: decoder)) + case "event": + self = .event(try AutomationEventTrigger(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown AutomationTrigger discriminant: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .schedule(var value): + value.kind = .schedule + try value.encode(to: encoder) + case .event(var value): + value.kind = .event + try value.encode(to: encoder) + } + } +} + +public enum AutomationRunCause: Codable, Sendable { + case manual(AutomationManualRunCause) + case trigger(AutomationTriggeredRunCause) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "kind" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "manual": + self = .manual(try AutomationManualRunCause(from: decoder)) + case "trigger": + self = .trigger(try AutomationTriggeredRunCause(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown AutomationRunCause discriminant: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .manual(var value): + value.kind = .manual + try value.encode(to: encoder) + case .trigger(var value): + value.kind = .trigger + try value.encode(to: encoder) + } + } +} + +public enum AutomationRunLifecycle: Codable, Sendable { + case pending(AutomationPendingRunLifecycle) + case running(AutomationRunningRunLifecycle) + case blocked(AutomationBlockedRunLifecycle) + case completed(AutomationCompletedRunLifecycle) + case failed(AutomationFailedRunLifecycle) + case cancelled(AutomationCancelledRunLifecycle) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "status" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "pending": + self = .pending(try AutomationPendingRunLifecycle(from: decoder)) + case "running": + self = .running(try AutomationRunningRunLifecycle(from: decoder)) + case "blocked": + self = .blocked(try AutomationBlockedRunLifecycle(from: decoder)) + case "completed": + self = .completed(try AutomationCompletedRunLifecycle(from: decoder)) + case "failed": + self = .failed(try AutomationFailedRunLifecycle(from: decoder)) + case "cancelled": + self = .cancelled(try AutomationCancelledRunLifecycle(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown AutomationRunLifecycle discriminant: \(discriminant)") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .pending(var value): + value.status = .pending + try value.encode(to: encoder) + case .running(var value): + value.status = .running + try value.encode(to: encoder) + case .blocked(var value): + value.status = .blocked + try value.encode(to: encoder) + case .completed(var value): + value.status = .completed + try value.encode(to: encoder) + case .failed(var value): + value.status = .failed + try value.encode(to: encoder) + case .cancelled(var value): + value.status = .cancelled + try value.encode(to: encoder) + } + } +} + public enum ToolResultContent: Codable, Sendable { case text(ToolResultTextContent) case embeddedResource(ToolResultEmbeddedResourceContent) @@ -6034,7 +6954,7 @@ public enum ToolResultContent: Codable, Sendable { } } -/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, annotations, or content state. +/// The state payload of a snapshot. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) @@ -6043,6 +6963,8 @@ public enum SnapshotState: Codable, Sendable { case changeset(ChangesetState) case resourceWatch(ResourceWatchState) case annotations(AnnotationsState) + case automation(AutomationState) + case automationRun(AutomationRunState) public init(from decoder: Decoder) throws { // Try the most distinctive shapes first. SessionState has required @@ -6061,6 +6983,10 @@ public enum SnapshotState: Codable, Sendable { self = .resourceWatch(resourceWatch) } else if let annotations = try? AnnotationsState(from: decoder) { self = .annotations(annotations) + } else if let automation = try? AutomationState(from: decoder) { + self = .automation(automation) + } else if let automationRun = try? AutomationRunState(from: decoder) { + self = .automationRun(automationRun) } else { self = .root(try RootState(from: decoder)) } @@ -6075,6 +7001,8 @@ public enum SnapshotState: Codable, Sendable { case .changeset(let state): try state.encode(to: encoder) case .resourceWatch(let state): try state.encode(to: encoder) case .annotations(let state): try state.encode(to: encoder) + case .automation(let state): try state.encode(to: encoder) + case .automationRun(let state): try state.encode(to: encoder) } } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 4dfa1c2c7..6dfa7f314 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -900,6 +900,7 @@ public let clientDispatchableActions: Set = [ "session/mcpServerStopRequested", "session/isReadChanged", "session/isArchivedChanged", + "automationRun/cancelRequested", ] /// Checks whether an action may be dispatched by a client. @@ -915,7 +916,8 @@ public func isClientDispatchable(_ action: StateAction) -> Bool { .sessionCustomizationToggled, .sessionMcpServerStartRequested, .sessionMcpServerStopRequested, .sessionIsReadChanged, - .sessionIsArchivedChanged: + .sessionIsArchivedChanged, + .automationRunCancelRequested: return true default: return false @@ -1444,3 +1446,70 @@ public func resourceWatchReducer(state: ResourceWatchState, action: StateAction) return state } } + +/// Pure reducer for automation state. +public func automationReducer(state: AutomationState, action: StateAction) -> AutomationState { + var next = state + switch action { + case .automationDefinitionChanged(let value): + next.definition = value.definition + next.revision = value.revision + next.modifiedAt = value.modifiedAt + next.nextRunAt = value.nextRunAt + case .automationRunSummarySet(let value): + if let index = next.runs.firstIndex(where: { $0.resource == value.run.resource }) { + next.runs[index] = value.run + } else { + next.runs.insert(value.run, at: 0) + } + case .automationRunSummaryRemoved(let value): + guard let index = next.runs.firstIndex(where: { $0.resource == value.run }) else { + return state + } + next.runs.remove(at: index) + case .automationRunsLoaded(let value): + var known = Set(next.runs.map(\.resource)) + next.runs.append(contentsOf: value.runs.filter { known.insert($0.resource).inserted }) + next.runsNextCursor = value.nextCursor + default: + return state + } + return next +} + +/// Pure reducer for automation-run state. +public func automationRunReducer(state: AutomationRunState, action: StateAction) -> AutomationRunState { + var next = state + switch action { + case .automationRunLifecycleChanged(let value): + next.lifecycle = value.lifecycle + next.operations = value.operations + case .automationRunSessionSet(let value): + guard !next.sessions.contains(value.session) else { return state } + next.sessions.append(value.session) + case .automationRunSessionRemoved(let value): + guard let index = next.sessions.firstIndex(of: value.session) else { return state } + next.sessions.remove(at: index) + if next.primarySession == value.session { + next.primarySession = nil + } + case .automationRunPrimarySessionChanged(let value): + next.primarySession = value.primarySession + case .automationRunArtifactSet(let value): + if let index = next.artifacts.firstIndex(where: { $0.id == value.artifact.id }) { + next.artifacts[index] = value.artifact + } else { + next.artifacts.append(value.artifact) + } + case .automationRunArtifactRemoved(let value): + guard let index = next.artifacts.firstIndex(where: { $0.id == value.artifactId }) else { + return state + } + next.artifacts.remove(at: index) + case .automationRunCancelRequested: + return state + default: + return state + } + return next +} diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClient.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClient.swift index 7e7b5e385..f7a2d7eea 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClient.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClient.swift @@ -951,6 +951,27 @@ public actor AHPClient { wrap: SubscriptionEvent.sessionSummaryChanged, channel: { $0.channel } ) + case "root/automationAdded": + await handleSubscriptionParams( + paramsData: paramsData, + type: AutomationAddedParams.self, + wrap: SubscriptionEvent.automationAdded, + channel: { $0.channel } + ) + case "root/automationRemoved": + await handleSubscriptionParams( + paramsData: paramsData, + type: AutomationRemovedParams.self, + wrap: SubscriptionEvent.automationRemoved, + channel: { $0.channel } + ) + case "root/automationSummaryChanged": + await handleSubscriptionParams( + paramsData: paramsData, + type: AutomationSummaryChangedParams.self, + wrap: SubscriptionEvent.automationSummaryChanged, + channel: { $0.channel } + ) case "auth/required": await handleSubscriptionParams( paramsData: paramsData, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClientEvents.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClientEvents.swift index 338f13b2e..dceeb33f8 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClientEvents.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPClientEvents.swift @@ -14,6 +14,9 @@ public enum SubscriptionEvent: Sendable { case sessionAdded(SessionAddedParams) case sessionRemoved(SessionRemovedParams) case sessionSummaryChanged(SessionSummaryChangedParams) + case automationAdded(AutomationAddedParams) + case automationRemoved(AutomationRemovedParams) + case automationSummaryChanged(AutomationSummaryChangedParams) case authRequired(AuthRequiredParams) } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift index 7c44fb33e..0792043e8 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/AHPStateMirror.swift @@ -8,8 +8,8 @@ import Foundation import AgentHostProtocol -/// In-memory mirror of root/session/terminal state, fed by `ActionEnvelope` -/// and `Snapshot` values from `AHPClient`. +/// In-memory mirror of stateful AHP channels, fed by `ActionEnvelope` and +/// `Snapshot` values from `AHPClient`. public actor AHPStateMirror { public private(set) var rootState: RootState = RootState(agents: []) public private(set) var sessions: [String: SessionState] = [:] @@ -18,6 +18,8 @@ public actor AHPStateMirror { public private(set) var changesets: [String: ChangesetState] = [:] public private(set) var annotations: [String: AnnotationsState] = [:] public private(set) var resourceWatches: [String: ResourceWatchState] = [:] + public private(set) var automations: [String: AutomationState] = [:] + public private(set) var automationRuns: [String: AutomationRunState] = [:] public init() {} @@ -65,11 +67,19 @@ public actor AHPStateMirror { // a reducer input. The slot is seeded by `applySnapshot`. return } + if var automation = automations[channel] { + automation = automationReducer(state: automation, action: action) + automations[channel] = automation + return + } + if var run = automationRuns[channel] { + run = automationRunReducer(state: run, action: action) + automationRuns[channel] = run + return + } } - /// Seed the mirror from a `Snapshot` — root, session, terminal, - /// changeset, resource-watch, or annotations as the snapshot's - /// `state` discriminator dictates. + /// Seed the mirror from a `Snapshot`, routing by its `state` discriminator. public func applySnapshot(_ snapshot: Snapshot) { switch snapshot.state { case .root(let state): @@ -86,6 +96,10 @@ public actor AHPStateMirror { resourceWatches[snapshot.resource] = state case .annotations(let state): annotations[snapshot.resource] = state + case .automation(let state): + automations[snapshot.resource] = state + case .automationRun(let state): + automationRuns[snapshot.resource] = state } } @@ -98,5 +112,7 @@ public actor AHPStateMirror { changesets.removeAll() annotations.removeAll() resourceWatches.removeAll() + automations.removeAll() + automationRuns.removeAll() } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostHandle.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostHandle.swift index 5e85893bf..1b417ec21 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostHandle.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostHandle.swift @@ -7,8 +7,8 @@ import AgentHostProtocol /// Snapshot of everything the multi-host SDK knows about a single host. /// /// This is the value type UIs render: connection state, last error, protocol -/// version, agents pulled from root state, subscribed URIs, cached session -/// summaries, and so on. +/// version, host capabilities, agents pulled from root state, subscribed URIs, +/// cached session summaries, and so on. /// /// Snapshots are immutable; refresh by calling /// `MultiHostClient.host(_:)`/`MultiHostClient.hosts()` again or by listening @@ -35,6 +35,8 @@ public struct HostHandle: Sendable { public let serverSeq: Int /// Optional `defaultDirectory` from the host's `InitializeResult`. public let defaultDirectory: String? + /// Automation support advertised by the host. + public let automations: AutomationCapabilities? /// Agents currently advertised by the host (mirrored from root state). public let agents: [AgentInfo] /// Active session count from root state, when present. @@ -64,6 +66,7 @@ public struct HostHandle: Sendable { protocolVersion: String?, serverSeq: Int, defaultDirectory: String?, + automations: AutomationCapabilities? = nil, agents: [AgentInfo], activeSessions: Int?, terminals: [TerminalInfo]?, @@ -81,6 +84,7 @@ public struct HostHandle: Sendable { self.protocolVersion = protocolVersion self.serverSeq = serverSeq self.defaultDirectory = defaultDirectory + self.automations = automations self.agents = agents self.activeSessions = activeSessions self.terminals = terminals diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift index 6bb1333fc..d3154e8c3 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostRuntime.swift @@ -71,6 +71,7 @@ internal final class HostRuntime: Sendable { protocolVersion: nil, serverSeq: 0, defaultDirectory: nil, + automations: nil, rootState: RootState(agents: []), subscriptions: config.initialSubscriptions, completionTriggerCharacters: [], @@ -371,6 +372,7 @@ internal final class HostRuntime: Sendable { if let init1 = initResult { state.protocolVersion = init1.protocolVersion state.defaultDirectory = init1.defaultDirectory + state.automations = init1.automations state.completionTriggerCharacters = init1.completionTriggerCharacters ?? [] if let snap = init1.snapshots.first(where: { $0.resource == RootResourceURI }) { if case .root(let root) = snap.state { @@ -606,6 +608,8 @@ internal final class HostRuntime: Sendable { state.sessionSummaries[n.session] = existing } } + case .automationAdded, .automationRemoved, .automationSummaryChanged: + break case .authRequired: break } @@ -826,9 +830,9 @@ private func applySummaryChanges( if let v = changes.title { existing.title = v } if let v = changes.status { existing.status = v } if let v = changes.activity { existing.activity = v } + if let v = changes.origin { existing.origin = v } if let v = changes.modifiedAt { existing.modifiedAt = v } if let v = changes.project { existing.project = v } if let v = changes.annotations { existing.annotations = v } if let v = changes.workingDirectories { existing.workingDirectories = v } } - diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostShared.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostShared.swift index d99c73d1c..cc5e3676e 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostShared.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/HostShared.swift @@ -21,6 +21,7 @@ internal struct HostInternal { var protocolVersion: String? var serverSeq: Int var defaultDirectory: String? + var automations: AutomationCapabilities? var rootState: RootState var subscriptions: [String] var completionTriggerCharacters: [String] @@ -44,6 +45,7 @@ internal struct HostInternal { protocolVersion: protocolVersion, serverSeq: serverSeq, defaultDirectory: defaultDirectory, + automations: automations, agents: rootState.agents, activeSessions: rootState.activeSessions, terminals: rootState.terminals, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/MultiHostClient.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/MultiHostClient.swift index 74afdb7ea..064dd960c 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/MultiHostClient.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/Hosts/MultiHostClient.swift @@ -548,7 +548,11 @@ public actor MultiHostClient { if let snap = await self.host(host) { cont.yield(snap.sessionSummaries) } - case .action, .authRequired: + case .action, + .automationAdded, + .automationRemoved, + .automationSummaryChanged, + .authRequired: continue } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift index 767b7ef2e..d671db6c2 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocolClient/MultiHostStateMirror.swift @@ -27,9 +27,8 @@ public struct HostedResourceKey: Hashable, Sendable { } } -/// In-memory mirror of root/session/terminal/changeset state, fed by -/// `ActionEnvelope` and `Snapshot` values tagged with their host of -/// origin. +/// In-memory mirror of stateful AHP channels, fed by `ActionEnvelope` and +/// `Snapshot` values tagged with their host of origin. /// /// Single-host consumers should keep using `AHPStateMirror`; this type /// adds the host dimension necessary for multi-host UIs. Apply @@ -50,6 +49,8 @@ public actor MultiHostStateMirror { public private(set) var changesets: [HostedResourceKey: ChangesetState] = [:] public private(set) var annotations: [HostedResourceKey: AnnotationsState] = [:] public private(set) var resourceWatches: [HostedResourceKey: ResourceWatchState] = [:] + public private(set) var automations: [HostedResourceKey: AutomationState] = [:] + public private(set) var automationRuns: [HostedResourceKey: AutomationRunState] = [:] public init() {} @@ -106,13 +107,22 @@ public actor MultiHostStateMirror { // a reducer input. The slot is seeded by `applySnapshot`. return } + if var automation = automations[key] { + automation = automationReducer(state: automation, action: action) + automations[key] = automation + return + } + if var run = automationRuns[key] { + run = automationRunReducer(state: run, action: action) + automationRuns[key] = run + return + } // No state for this `(host, channel)` yet — the reducer can't // initialise one; only `applySnapshot(host:snapshot:)` can. } - /// Seed the mirror from a `Snapshot` scoped to `host` — root, - /// session, terminal, changeset, resource-watch, or annotations as - /// the snapshot's `state` discriminator dictates. + /// Seed the mirror from a `Snapshot` scoped to `host`, routing by its + /// `state` discriminator. public func applySnapshot(host: HostId, snapshot: Snapshot) { let key = HostedResourceKey(hostId: host, uri: snapshot.resource) switch snapshot.state { @@ -130,13 +140,14 @@ public actor MultiHostStateMirror { resourceWatches[key] = state case .annotations(let state): annotations[key] = state + case .automation(let state): + automations[key] = state + case .automationRun(let state): + automationRuns[key] = state } } - /// Reset every slot for `host` — drops the root state, all sessions - /// keyed under that host, all terminals keyed under that host, all - /// changesets keyed under that host, all annotations keyed under - /// that host, and all resource watches keyed under that host. + /// Reset every state slot keyed under `host`. public func reset(host: HostId) { rootStates.removeValue(forKey: host) sessions = sessions.filter { $0.key.hostId != host } @@ -145,6 +156,8 @@ public actor MultiHostStateMirror { changesets = changesets.filter { $0.key.hostId != host } annotations = annotations.filter { $0.key.hostId != host } resourceWatches = resourceWatches.filter { $0.key.hostId != host } + automations = automations.filter { $0.key.hostId != host } + automationRuns = automationRuns.filter { $0.key.hostId != host } } /// Reset every host's state. @@ -156,5 +169,7 @@ public actor MultiHostStateMirror { changesets.removeAll() annotations.removeAll() resourceWatches.removeAll() + automations.removeAll() + automationRuns.removeAll() } } diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift index 5f1323dfe..77c96ef52 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/AHPClientTests.swift @@ -188,6 +188,103 @@ final class AHPClientTests: XCTestCase { await client.shutdown() } + func testAutomationCatalogueNotificationsDispatchToSubscriptionsAndEvents() async throws { + let (clientSide, serverSide) = InMemoryTransport.pair() + let client = AHPClient(transport: clientSide) + let events = await client.events + let subscription = await client.attachSubscription(RootResourceURI) + try await client.connect() + + let initial = AutomationSummary( + resource: "ahp-automation:/a1", + title: "Initial", + enabled: true, + triggerCount: 0, + revision: 1, + operations: [.run], + createdAt: "2026-08-05T12:00:00Z", + modifiedAt: "2026-08-05T12:00:00Z" + ) + let changed = AutomationSummary( + resource: initial.resource, + title: "Changed", + enabled: false, + triggerCount: 1, + revision: 2, + operations: [.update, .run], + createdAt: initial.createdAt, + modifiedAt: "2026-08-05T13:00:00Z" + ) + + let serverTask = Task { + try await pushNotification( + method: "root/automationAdded", + params: AutomationAddedParams(channel: RootResourceURI, summary: initial), + on: serverSide + ) + try await pushNotification( + method: "root/automationRemoved", + params: AutomationRemovedParams( + channel: RootResourceURI, + automation: initial.resource + ), + on: serverSide + ) + try await pushNotification( + method: "root/automationSummaryChanged", + params: AutomationSummaryChangedParams( + channel: RootResourceURI, + summary: changed + ), + on: serverSide + ) + } + + var subscriptionIter = subscription.makeAsyncIterator() + let added = try await nextWithTimeout(&subscriptionIter) + guard case .automationAdded(let addedParams) = added else { + XCTFail("expected automationAdded, got \(String(describing: added))") + return + } + XCTAssertEqual(addedParams.summary.resource, initial.resource) + + let removed = try await nextWithTimeout(&subscriptionIter) + guard case .automationRemoved(let removedParams) = removed else { + XCTFail("expected automationRemoved, got \(String(describing: removed))") + return + } + XCTAssertEqual(removedParams.automation, initial.resource) + + let summaryChanged = try await nextWithTimeout(&subscriptionIter) + guard case .automationSummaryChanged(let changedParams) = summaryChanged else { + XCTFail("expected automationSummaryChanged, got \(String(describing: summaryChanged))") + return + } + XCTAssertEqual(changedParams.summary.title, "Changed") + + var eventIter = events.makeAsyncIterator() + var receivedKinds: Set = [] + for _ in 0..<3 { + let nextEvent = try await nextWithTimeout(&eventIter) + let event = try XCTUnwrap(nextEvent) + XCTAssertEqual(event.resource, RootResourceURI) + switch event.event { + case .automationAdded: + receivedKinds.insert("added") + case .automationRemoved: + receivedKinds.insert("removed") + case .automationSummaryChanged: + receivedKinds.insert("changed") + default: + XCTFail("unexpected event: \(event.event)") + } + } + XCTAssertEqual(receivedKinds, ["added", "removed", "changed"]) + + try await serverTask.value + await client.shutdown() + } + // MARK: - unexpected_close_fails_pending_requests func testUnexpectedCloseFailsPendingRequests() async throws { diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostClientTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostClientTests.swift index 598377231..0b14c26ba 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostClientTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostClientTests.swift @@ -132,6 +132,46 @@ final class MultiHostClientTests: XCTestCase { await multi.shutdown() } + func testAutomationCapabilitiesPersistAcrossReconnect() async throws { + let mode = ReconnectResponseModeSwitch() + let capabilities = AutomationCapabilities( + execution: AutomationExecutionCapabilities(lifetime: .managed), + create: AutomationCreateCapability(), + runHistoryLimit: 25 + ) + let multi = MultiHostClient() + let config = HostConfig( + id: "local", + label: "Local", + transportFactory: makeReconnectResultFactory( + mode: mode, + automationCapabilities: capabilities + ) + ).withInitialSubscriptions([RootResourceURI]) + _ = try await multi.add(config) + await waitForHostState(multi, id: "local") { $0.isConnected } + + let initialValue = await multi.host("local") + let initial = try XCTUnwrap(initialValue) + XCTAssertEqual(initial.automations?.execution.lifetime, .managed) + XCTAssertNotNil(initial.automations?.create) + XCTAssertEqual(initial.automations?.runHistoryLimit, 25) + + try await multi.reconnect("local") + await waitUntil { + guard let snapshot = await multi.host("local") else { return false } + return snapshot.generation > initial.generation && snapshot.state.isConnected + } + + let reconnectedValue = await multi.host("local") + let reconnected = try XCTUnwrap(reconnectedValue) + XCTAssertEqual(reconnected.automations?.execution.lifetime, .managed) + XCTAssertNotNil(reconnected.automations?.create) + XCTAssertEqual(reconnected.automations?.runHistoryLimit, 25) + + await multi.shutdown() + } + // MARK: - dispatch_can_use_explicit_client_seq func testDispatchCanUseExplicitClientSeqThroughMultiHostSurfaces() async throws { @@ -1116,17 +1156,27 @@ private func actionEnvelope(from event: HostSubscriptionEvent?) -> ActionEnvelop // MARK: - Reconnect-result fake host -private func makeReconnectResultFactory(mode: ReconnectResponseModeSwitch) -> HostTransportFactory { +private func makeReconnectResultFactory( + mode: ReconnectResponseModeSwitch, + automationCapabilities: AutomationCapabilities? = nil +) -> HostTransportFactory { { _ in let (clientSide, serverSide) = InMemoryTransport.pair() - Task { await driveReconnectResultHost(transport: serverSide, mode: mode) } + Task { + await driveReconnectResultHost( + transport: serverSide, + mode: mode, + automationCapabilities: automationCapabilities + ) + } return clientSide } } private func driveReconnectResultHost( transport: InMemoryTransport, - mode: ReconnectResponseModeSwitch + mode: ReconnectResponseModeSwitch, + automationCapabilities: AutomationCapabilities? ) async { while !Task.isCancelled { let frame: TransportMessage? @@ -1147,7 +1197,11 @@ private func driveReconnectResultHost( let result: Any switch method { case "initialize": - result = initializeResult(serverSeq: 40, activeSessions: 1) + result = initializeResult( + serverSeq: 40, + activeSessions: 1, + automationCapabilities: automationCapabilities + ) case "reconnect": result = reconnectResult(for: currentMode) case "listSessions": @@ -1168,12 +1222,22 @@ private func driveReconnectResultHost( } } -private func initializeResult(serverSeq: Int, activeSessions: Int) -> [String: Any] { - [ +private func initializeResult( + serverSeq: Int, + activeSessions: Int, + automationCapabilities: AutomationCapabilities? = nil +) -> [String: Any] { + var result: [String: Any] = [ "protocolVersion": "0.1.0", "serverSeq": serverSeq, "snapshots": [rootSnapshot(fromSeq: serverSeq, activeSessions: activeSessions)], - ] as [String: Any] + ] + if let automationCapabilities, + let data = try? JSONEncoder().encode(automationCapabilities), + let object = try? JSONSerialization.jsonObject(with: data) { + result["automations"] = object + } + return result } private func reconnectResult(for mode: ReconnectResponseMode) -> [String: Any] { diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift index 25ff0b871..e0c423194 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/MultiHostStateMirrorTests.swift @@ -119,6 +119,85 @@ final class MultiHostStateMirrorTests: XCTestCase { "session-scoped action on alpha must not touch beta's identically-named session") } + func testAutomationAndRunActionsUpdateMirroredSnapshots() async { + let mirror = MultiHostStateMirror() + let automationResource = "ahp-automation:/a1" + let runResource = "ahp-automation-run:/r1" + let initialDefinition = makeAutomationDefinition(title: "Old") + let initialLifecycle = AutomationRunLifecycle.pending(AutomationPendingRunLifecycle( + status: .pending, + createdAt: "2026-08-05T12:00:00Z" + )) + + await mirror.applySnapshot( + host: "alpha", + snapshot: Snapshot( + resource: automationResource, + state: .automation(AutomationState( + resource: automationResource, + definition: initialDefinition, + revision: 1, + nextRunAt: "2026-08-06T12:00:00Z", + runs: [], + operations: [.update, .run], + createdAt: "2026-08-05T12:00:00Z", + modifiedAt: "2026-08-05T12:00:00Z" + )), + fromSeq: 0 + ) + ) + await mirror.applySnapshot( + host: "alpha", + snapshot: Snapshot( + resource: runResource, + state: .automationRun(AutomationRunState( + resource: runResource, + automation: automationResource, + cause: .manual(AutomationManualRunCause(kind: .manual)), + lifecycle: initialLifecycle, + sessions: [], + artifacts: [], + operations: [.cancel] + )), + fromSeq: 0 + ) + ) + + await mirror.apply( + host: "alpha", + envelope: ActionEnvelope( + channel: automationResource, + action: .automationDefinitionChanged(AutomationDefinitionChangedAction( + type: .automationDefinitionChanged, + definition: makeAutomationDefinition(title: "New"), + revision: 2, + modifiedAt: "2026-08-05T13:00:00Z" + )), + serverSeq: 1 + ) + ) + await mirror.apply( + host: "alpha", + envelope: ActionEnvelope( + channel: runResource, + action: .automationRunSessionSet(AutomationRunSessionSetAction( + type: .automationRunSessionSet, + session: "ahp-session:/s1" + )), + serverSeq: 2 + ) + ) + + let key = HostedResourceKey(hostId: "alpha", uri: automationResource) + let runKey = HostedResourceKey(hostId: "alpha", uri: runResource) + let automations = await mirror.automations + let runs = await mirror.automationRuns + XCTAssertEqual(automations[key]?.definition.title, "New") + XCTAssertEqual(automations[key]?.revision, 2) + XCTAssertNil(automations[key]?.nextRunAt) + XCTAssertEqual(runs[runKey]?.sessions, ["ahp-session:/s1"]) + } + // MARK: - apply_host_subscription_event_forwards_to_per_host_apply func testApplyHostSubscriptionEventForwardsToPerHostApply() async { @@ -166,3 +245,13 @@ final class MultiHostStateMirrorTests: XCTestCase { XCTAssertNotNil(roots["beta"]) } } + +private func makeAutomationDefinition(title: String) -> AutomationDefinition { + AutomationDefinition( + title: title, + message: Message(text: "Run", origin: MessageOrigin(kind: .user)), + session: AutomationSessionTemplate(), + enabled: true, + triggers: [] + ) +} diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift index 47a9e481b..fc3031a3f 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift @@ -190,6 +190,8 @@ final class TypesRoundTripFixtureTests: XCTestCase { return try reencode(dec.decode(InitializeResult.self, from: inputData)) case "ChatSource": return try reencode(dec.decode(ChatSource.self, from: inputData)) + case "Snapshot": + return try reencode(dec.decode(Snapshot.self, from: inputData)) default: throw FixtureError.message( "round-trip fixture: unknown wire type \"\(type)\". Add a decode entry to decodeAndReencode.") diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift new file mode 100644 index 000000000..41b28b255 --- /dev/null +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift @@ -0,0 +1,16 @@ +import Foundation +import XCTest +@testable import AgentHostProtocol + +final class AutomationUnionTests: XCTestCase { + func testScheduleEncodesUnionDiscriminator() throws { + let schedule = AutomationSchedule.daily(AutomationDailySchedule( + kind: .hourly, + time: AutomationLocalTime(hour: 9, minute: 0), + timeZone: "UTC" + )) + let data = try JSONEncoder().encode(schedule) + let object = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) + XCTAssertEqual(object["kind"] as? String, "daily") + } +} diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift index 4eecdaabf..261729d0e 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/FixtureDrivenReducerTests.swift @@ -214,6 +214,14 @@ final class FixtureDrivenReducerTests: XCTestCase { try compareFixture(file: file, fixture: fixture, stateType: AnnotationsState.self) { state in actions.reduce(state) { annotationsReducer(state: $0, action: $1) } } + case "automation": + try compareFixture(file: file, fixture: fixture, stateType: AutomationState.self) { state in + actions.reduce(state) { automationReducer(state: $0, action: $1) } + } + case "automationRun": + try compareFixture(file: file, fixture: fixture, stateType: AutomationRunState.self) { state in + actions.reduce(state) { automationRunReducer(state: $0, action: $1) } + } default: throw FixtureError.unsupportedReducer(fixture.reducer) } diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift index b234db366..125f8be6b 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift @@ -95,6 +95,13 @@ final class ReducersTests: XCTestCase { XCTAssertTrue(isClientDispatchable(action)) } + func testAutomationCancellationIsClientDispatchable() { + let action: StateAction = .automationRunCancelRequested( + AutomationRunCancelRequestedAction(type: .automationRunCancelRequested) + ) + XCTAssertTrue(isClientDispatchable(action)) + } + func testClientDispatchableReturnsFalse() { let action: StateAction = .sessionReady(SessionReadyAction(type: .sessionReady)) XCTAssertFalse(isClientDispatchable(action)) diff --git a/clients/typescript/src/client/client.ts b/clients/typescript/src/client/client.ts index 36066b583..806d4c128 100644 --- a/clients/typescript/src/client/client.ts +++ b/clients/typescript/src/client/client.ts @@ -70,6 +70,9 @@ import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, + AutomationAddedParams, + AutomationRemovedParams, + AutomationSummaryChangedParams, } from '../types/channels-root/notifications.js'; import type { AuthRequiredParams } from '../types/common/notifications.js'; import type { URI } from '../types/common/state.js'; @@ -847,6 +850,21 @@ export class AhpClient { this.fanOut(p.channel, { type: 'sessionSummaryChanged', params: p }); break; } + case 'root/automationAdded': { + const p = n.params as AutomationAddedParams; + this.fanOut(p.channel, { type: 'automationAdded', params: p }); + break; + } + case 'root/automationRemoved': { + const p = n.params as AutomationRemovedParams; + this.fanOut(p.channel, { type: 'automationRemoved', params: p }); + break; + } + case 'root/automationSummaryChanged': { + const p = n.params as AutomationSummaryChangedParams; + this.fanOut(p.channel, { type: 'automationSummaryChanged', params: p }); + break; + } case 'auth/required': { const p = n.params as AuthRequiredParams; this.fanOut(p.channel, { type: 'authRequired', params: p }); diff --git a/clients/typescript/src/client/events.ts b/clients/typescript/src/client/events.ts index d005a8b39..498ae2661 100644 --- a/clients/typescript/src/client/events.ts +++ b/clients/typescript/src/client/events.ts @@ -16,6 +16,9 @@ import type { SessionAddedParams, SessionRemovedParams, SessionSummaryChangedParams, + AutomationAddedParams, + AutomationRemovedParams, + AutomationSummaryChangedParams, } from '../types/channels-root/notifications.js'; import type { AuthRequiredParams } from '../types/common/notifications.js'; import type { URI } from '../types/common/state.js'; @@ -27,6 +30,9 @@ export type SubscriptionEvent = | { readonly type: 'sessionAdded'; readonly params: SessionAddedParams } | { readonly type: 'sessionRemoved'; readonly params: SessionRemovedParams } | { readonly type: 'sessionSummaryChanged'; readonly params: SessionSummaryChangedParams } + | { readonly type: 'automationAdded'; readonly params: AutomationAddedParams } + | { readonly type: 'automationRemoved'; readonly params: AutomationRemovedParams } + | { readonly type: 'automationSummaryChanged'; readonly params: AutomationSummaryChangedParams } | { readonly type: 'authRequired'; readonly params: AuthRequiredParams }; /** diff --git a/clients/typescript/src/client/hosts/runtime.ts b/clients/typescript/src/client/hosts/runtime.ts index 09e3ae830..52f26fdb0 100644 --- a/clients/typescript/src/client/hosts/runtime.ts +++ b/clients/typescript/src/client/hosts/runtime.ts @@ -36,6 +36,7 @@ import type { AsyncBroadcastQueue } from '../async-queue.js'; import { rootReducer } from '../../types/channels-root/reducer.js'; import type { RootAction } from '../../types/action-origin.generated.js'; import type { StateAction } from '../../types/common/actions.js'; +import type { AutomationCapabilities } from '../../types/common/commands.js'; import { HostNotConnectedError, HostShutDownError, @@ -71,6 +72,7 @@ export interface HostShared { protocolVersion: string | null; serverSeq: number; defaultDirectory: string | null; + automations: AutomationCapabilities | null; rootState: RootState; subscriptions: URI[]; completionTriggerCharacters: string[]; @@ -101,6 +103,7 @@ export function makeInitialShared( protocolVersion: null, serverSeq: 0, defaultDirectory: null, + automations: null, rootState: { agents: [] }, subscriptions: [...config.initialSubscriptions], completionTriggerCharacters: [], @@ -123,6 +126,7 @@ export function snapshotHandle(shared: HostShared): HostHandle { protocolVersion: shared.protocolVersion, serverSeq: shared.serverSeq, defaultDirectory: shared.defaultDirectory, + automations: shared.automations, agents: [...shared.rootState.agents], activeSessions: shared.rootState.activeSessions ?? null, terminals: shared.rootState.terminals ? [...shared.rootState.terminals] : null, @@ -569,6 +573,7 @@ export class HostRuntime { let initServerSeq = prior.serverSeq; let initProtocolVersion: string | null = null; let initDefaultDirectory: string | null = null; + let initAutomations = this.shared.automations; let initCompletionTriggers: string[] = []; if (canReconnect) { @@ -603,6 +608,7 @@ export class HostRuntime { initServerSeq = initResult.serverSeq; initProtocolVersion = initResult.protocolVersion; initDefaultDirectory = initResult.defaultDirectory ?? null; + initAutomations = initResult.automations ?? null; initCompletionTriggers = initResult.completionTriggerCharacters ?? []; } } else { @@ -619,6 +625,7 @@ export class HostRuntime { initServerSeq = initResult.serverSeq; initProtocolVersion = initResult.protocolVersion; initDefaultDirectory = initResult.defaultDirectory ?? null; + initAutomations = initResult.automations ?? null; initCompletionTriggers = initResult.completionTriggerCharacters ?? []; } @@ -684,6 +691,7 @@ export class HostRuntime { if (rootSnap) this.shared.rootState = (rootSnap.state as RootState) ?? EMPTY_ROOT_STATE; if (initProtocolVersion) this.shared.protocolVersion = initProtocolVersion; this.shared.defaultDirectory = initDefaultDirectory; + this.shared.automations = initAutomations; this.shared.completionTriggerCharacters = [...initCompletionTriggers]; } if (summaries !== null) { @@ -807,6 +815,9 @@ export class HostRuntime { case 'sessionSummaryChanged': applySummaryChange(this.shared.sessionSummaries, event.event.params); break; + case 'automationAdded': + case 'automationRemoved': + case 'automationSummaryChanged': case 'authRequired': // No cache update; consumers observe via the event stream. break; @@ -877,6 +888,7 @@ function applySummaryChange( if (changes.title !== undefined) merged.title = changes.title; if (changes.status !== undefined) merged.status = changes.status; if (changes.activity !== undefined) merged.activity = changes.activity; + if (changes.origin !== undefined) merged.origin = changes.origin; if (changes.modifiedAt !== undefined) merged.modifiedAt = changes.modifiedAt; if (changes.project !== undefined) merged.project = changes.project; if (changes.workingDirectories !== undefined) merged.workingDirectories = changes.workingDirectories; diff --git a/clients/typescript/src/client/hosts/state-mirror.ts b/clients/typescript/src/client/hosts/state-mirror.ts index e418205ba..9ceb8054b 100644 --- a/clients/typescript/src/client/hosts/state-mirror.ts +++ b/clients/typescript/src/client/hosts/state-mirror.ts @@ -32,6 +32,8 @@ import type { ActionEnvelope } from '../../types/common/actions.js'; import type { Snapshot, URI } from '../../types/common/state.js'; import type { ChangesetAction, + AutomationAction, + AutomationRunAction, RootAction, SessionAction, TerminalAction, @@ -40,10 +42,14 @@ import type { ChangesetState } from '../../types/channels-changeset/state.js'; import type { RootState } from '../../types/channels-root/state.js'; import type { SessionState } from '../../types/channels-session/state.js'; import type { TerminalState } from '../../types/channels-terminal/state.js'; +import type { AutomationState } from '../../types/channels-automation/state.js'; +import type { AutomationRunState } from '../../types/channels-automation-run/state.js'; import { changesetReducer } from '../../types/channels-changeset/reducer.js'; import { rootReducer } from '../../types/channels-root/reducer.js'; import { sessionReducer } from '../../types/channels-session/reducer.js'; import { terminalReducer } from '../../types/channels-terminal/reducer.js'; +import { automationReducer } from '../../types/channels-automation/reducer.js'; +import { automationRunReducer } from '../../types/channels-automation-run/reducer.js'; import { ROOT_RESOURCE_URI, type HostId, type HostSubscriptionEvent } from './types.js'; const INITIAL_ROOT: RootState = { agents: [] }; @@ -98,6 +104,8 @@ export class MultiHostStateMirror { private readonly sessionsMap = new Map(); private readonly terminalsMap = new Map(); private readonly changesetsMap = new Map(); + private readonly automationsMap = new Map(); + private readonly automationRunsMap = new Map(); /** All known root states keyed by host. */ get rootStates(): ReadonlyMap { @@ -119,6 +127,14 @@ export class MultiHostStateMirror { return this.changesetsMap; } + get automations(): ReadonlyMap { + return this.automationsMap; + } + + get automationRuns(): ReadonlyMap { + return this.automationRunsMap; + } + /** Look up the root state for `hostId`. */ getRoot(hostId: HostId): RootState | undefined { @@ -187,6 +203,20 @@ export class MultiHostStateMirror { this.changesetsMap.set(key, changesetReducer(current, action as ChangesetAction)); return; } + if (channel.startsWith('ahp-automation-run:')) { + const key = hostedResourceKey(hostId, channel); + const current = this.automationRunsMap.get(key); + if (!current) return; + this.automationRunsMap.set(key, automationRunReducer(current, action as AutomationRunAction)); + return; + } + if (channel.startsWith('ahp-automation:')) { + const key = hostedResourceKey(hostId, channel); + const current = this.automationsMap.get(key); + if (!current) return; + this.automationsMap.set(key, automationReducer(current, action as AutomationAction)); + return; + } } /** @@ -213,6 +243,14 @@ export class MultiHostStateMirror { this.changesetsMap.set(key, snapshot.state as ChangesetState); return; } + if (resource.startsWith('ahp-automation-run:')) { + this.automationRunsMap.set(key, snapshot.state as AutomationRunState); + return; + } + if (resource.startsWith('ahp-automation:')) { + this.automationsMap.set(key, snapshot.state as AutomationState); + return; + } } /** Drop every slot keyed under `hostId` — root, sessions, terminals, changesets. */ @@ -228,6 +266,12 @@ export class MultiHostStateMirror { for (const key of this.changesetsMap.keys()) { if (key.startsWith(prefix)) this.changesetsMap.delete(key); } + for (const key of this.automationsMap.keys()) { + if (key.startsWith(prefix)) this.automationsMap.delete(key); + } + for (const key of this.automationRunsMap.keys()) { + if (key.startsWith(prefix)) this.automationRunsMap.delete(key); + } } /** Drop every host's state. */ @@ -236,5 +280,7 @@ export class MultiHostStateMirror { this.sessionsMap.clear(); this.terminalsMap.clear(); this.changesetsMap.clear(); + this.automationsMap.clear(); + this.automationRunsMap.clear(); } } diff --git a/clients/typescript/src/client/hosts/types.ts b/clients/typescript/src/client/hosts/types.ts index cdd71b9b1..e4de8e753 100644 --- a/clients/typescript/src/client/hosts/types.ts +++ b/clients/typescript/src/client/hosts/types.ts @@ -8,6 +8,7 @@ import type { URI } from '../../types/common/state.js'; import type { AgentInfo } from '../../types/channels-root/state.js'; import type { TerminalInfo } from '../../types/channels-terminal/state.js'; import type { SessionSummary } from '../../types/channels-session/state.js'; +import type { AutomationCapabilities } from '../../types/common/commands.js'; import type { ClientEvent, SubscriptionEvent } from '../events.js'; import { AhpClientError } from '../error.js'; import type { ClientIdStore } from './client-id-store.js'; @@ -169,6 +170,8 @@ export interface HostHandle { readonly serverSeq: number; /** Optional `defaultDirectory` from the host's `InitializeResult`. */ readonly defaultDirectory: string | null; + /** Automation support advertised by the host. */ + readonly automations: AutomationCapabilities | null; /** Agents currently advertised by the host (mirrored from root state). */ readonly agents: readonly AgentInfo[]; /** Active session count from root state, when present. */ diff --git a/clients/typescript/src/client/state-mirror.ts b/clients/typescript/src/client/state-mirror.ts index c10839f34..c5b939fb4 100644 --- a/clients/typescript/src/client/state-mirror.ts +++ b/clients/typescript/src/client/state-mirror.ts @@ -17,6 +17,8 @@ import type { ActionEnvelope } from '../types/common/actions.js'; import type { Snapshot, URI } from '../types/common/state.js'; import type { ChangesetAction, + AutomationAction, + AutomationRunAction, RootAction, SessionAction, TerminalAction, @@ -25,10 +27,14 @@ import type { ChangesetState } from '../types/channels-changeset/state.js'; import type { RootState } from '../types/channels-root/state.js'; import type { SessionState } from '../types/channels-session/state.js'; import type { TerminalState } from '../types/channels-terminal/state.js'; +import type { AutomationState } from '../types/channels-automation/state.js'; +import type { AutomationRunState } from '../types/channels-automation-run/state.js'; import { changesetReducer } from '../types/channels-changeset/reducer.js'; import { rootReducer } from '../types/channels-root/reducer.js'; import { sessionReducer } from '../types/channels-session/reducer.js'; import { terminalReducer } from '../types/channels-terminal/reducer.js'; +import { automationReducer } from '../types/channels-automation/reducer.js'; +import { automationRunReducer } from '../types/channels-automation-run/reducer.js'; const ROOT_URI = 'ahp-root://' as const; @@ -40,6 +46,8 @@ export class AhpStateMirror { private readonly sessionsMap = new Map(); private readonly terminalsMap = new Map(); private readonly changesetsMap = new Map(); + private readonly automationsMap = new Map(); + private readonly automationRunsMap = new Map(); /** Current root state. */ get root(): RootState { @@ -61,6 +69,14 @@ export class AhpStateMirror { return this.changesetsMap; } + get automations(): ReadonlyMap { + return this.automationsMap; + } + + get automationRuns(): ReadonlyMap { + return this.automationRunsMap; + } + /** Look up a session by URI. */ getSession(uri: URI): SessionState | undefined { @@ -95,6 +111,14 @@ export class AhpStateMirror { this.changesetsMap.set(resource, snapshot.state as ChangesetState); return; } + if (resource.startsWith('ahp-automation-run:')) { + this.automationRunsMap.set(resource, snapshot.state as AutomationRunState); + return; + } + if (resource.startsWith('ahp-automation:')) { + this.automationsMap.set(resource, snapshot.state as AutomationState); + return; + } } /** @@ -131,5 +155,17 @@ export class AhpStateMirror { this.changesetsMap.set(channel, changesetReducer(current, action as ChangesetAction)); return; } + if (channel.startsWith('ahp-automation-run:')) { + const current = this.automationRunsMap.get(channel); + if (!current) return; + this.automationRunsMap.set(channel, automationRunReducer(current, action as AutomationRunAction)); + return; + } + if (channel.startsWith('ahp-automation:')) { + const current = this.automationsMap.get(channel); + if (!current) return; + this.automationsMap.set(channel, automationReducer(current, action as AutomationAction)); + return; + } } } diff --git a/clients/typescript/test/client.test.ts b/clients/typescript/test/client.test.ts index 1516ad65e..28ce1a2fb 100644 --- a/clients/typescript/test/client.test.ts +++ b/clients/typescript/test/client.test.ts @@ -113,6 +113,35 @@ test('subscribe attaches before sending the request and fans out an action', asy delivery: { maxLatencyMs: 100 }, view: { turns: 30 }, }); + + test('root automation catalogue notifications reach subscriptions', async () => { + const [c, s] = InMemoryTransport.pair(); + const client = new AhpClient(c); + client.connect(); + const subscription = client.attachSubscription(ROOT); + + pushNotification(s, 'root/automationAdded', { + channel: ROOT, + summary: { + resource: 'ahp-automation:/a1', + title: 'Daily triage', + enabled: true, + triggerCount: 1, + revision: 1, + operations: ['run'], + createdAt: '2026-08-01T00:00:00Z', + modifiedAt: '2026-08-01T00:00:00Z', + }, + }); + + const next = await subscription.next(); + assert.equal(next.done, false); + assert.equal(next.value?.type, 'automationAdded'); + if (next.value?.type !== 'automationAdded') throw new Error('unreachable'); + assert.equal(next.value.params.summary.resource, 'ahp-automation:/a1'); + + await client.shutdown(); + }); const req = await readRequest(s); assert.equal(req.method, 'subscribe'); assert.equal((req.params as SubscribeParams).channel, 'ahp-session:/s1'); diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts index 957c009ac..f5736a2be 100644 --- a/clients/typescript/test/types-round-trip.test.ts +++ b/clients/typescript/test/types-round-trip.test.ts @@ -51,7 +51,7 @@ import type { ActionEnvelope, StateAction, } from '../src/types/common/actions.js'; -import type { StringOrMarkdown } from '../src/types/common/state.js'; +import type { Snapshot, StringOrMarkdown } from '../src/types/common/state.js'; import type { ChangesetOperationTarget } from '../src/types/channels-changeset/commands.js'; import type { ChatInputQuestion, @@ -244,6 +244,7 @@ function bindToType(file: string, type: string, parsed: unknown): void { case 'Implementation': void (parsed as Implementation); break; case 'InitializeResult': void (parsed as InitializeResult); break; case 'ChatSource': void (parsed as ChatSource); break; + case 'Snapshot': void (parsed as Snapshot); break; default: throw new Error( `${file}: unknown wire type "${type}". Add a decode entry to bindToType.`, diff --git a/docs/.changes/20260805-shared-automations.json b/docs/.changes/20260805-shared-automations.json new file mode 100644 index 000000000..a5d3d73d2 --- /dev/null +++ b/docs/.changes/20260805-shared-automations.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Automation and automation-run channels for shared trigger-based agent session workflows." +} diff --git a/docs/guide/automations.md b/docs/guide/automations.md new file mode 100644 index 000000000..878938792 --- /dev/null +++ b/docs/guide/automations.md @@ -0,0 +1,46 @@ +# Automations + +Automations let several AHP clients share durable agent-session workflows +without each client running its own scheduler. + +## Mental model + +- An **automation** is a saved session template, initial user message, and + trigger collection. +- A **run** is one task-level execution. +- A **session** is one attempt or worker belonging to that run. + +```text +automation + -> run summary + -> automation-run channel + -> one or more session channels +``` + +The host is the single writable authority for definitions, trigger claims, +runs, and session links. Clients own presentation and confirmation UX. + +## Manual and automatic triggers + +An empty trigger collection is manual-only. Schedule triggers are portable +hourly, daily, weekly, or five-field cron schedules. Event triggers are +host-defined and retain unknown schema fields during edits. + +## Multiple clients + +Definitions use monotonic revisions. Manual run requests use durable +idempotency keys. Clients re-fetch catalogues after reconnect and never execute +a local fallback copy after an uncertain host response. + +## Availability + +`AutomationCapabilities.execution.lifetime` distinguishes a host-lifetime +scheduler from a managed service. This describes availability; it never moves +scheduling authority into clients. + +## Migration + +Move one definition to one authority. Import the host copy disabled, remove the +legacy copy from scheduler-visible storage, then enable the host copy. Never +dual-schedule or deduplicate by content. + diff --git a/docs/specification/automation-channel.md b/docs/specification/automation-channel.md new file mode 100644 index 000000000..c64b4b3ed --- /dev/null +++ b/docs/specification/automation-channel.md @@ -0,0 +1,68 @@ +# Automation Channel + +The automation channel represents a durable definition that launches fresh +agent sessions manually or from host-owned triggers. + +## URI + +```text +ahp-automation:/ +``` + +The client chooses the URI during `createAutomation`. The host owns persistence, +revision ordering, trigger evaluation, run claims, and run history. + +## State + +`AutomationState` contains the complete definition, monotonic revision, +host-computed next scheduled run, a newest-first window of +`AutomationRunSummary` entries, and allowed operations. + +An empty trigger list means manual-only. Schedule triggers are typed; event +triggers use a host-defined type plus schema-defined configuration returned by +`listAutomationTriggerDefinitions`. + +## Catalogue + +Clients fetch summaries through `listAutomations` on `ahp-root://`. Root +subscribers receive: + +- `root/automationAdded` +- `root/automationRemoved` +- `root/automationSummaryChanged` + +Catalogue notifications are not replayed. Clients re-fetch after reconnect. + +## Commands + +- `createAutomation` creates a client-chosen URI. +- `updateAutomation` applies a patch guarded by `expectedRevision`. +- `disposeAutomation` removes a definition with no active run. +- `runAutomation` idempotently creates a run by `requestId`. +- `fetchAutomationRuns` loads older summaries through + `automation/runsLoaded`. +- `listAutomationTriggerDefinitions` returns host trigger schemas. +- `previewAutomationSchedule` returns host-canonical future occurrences. + +## Actions + +- `automation/definitionChanged` +- `automation/runSummarySet` +- `automation/runSummaryRemoved` +- `automation/runsLoaded` + +The host sequences all actions. Automation actions are server-originated. + +## Scheduling + +Scheduling belongs to the host. Calendar schedules persist an IANA time zone. +`enabled` controls automatic triggers only; manual runs remain available when +the operation is advertised. A host atomically associates a scheduled +occurrence with at most one run. + +## Security + +Definitions contain no credentials or durable permission grants. The host +authorizes every operation and revalidates session configuration at execution +time. + diff --git a/docs/specification/automation-run-channel.md b/docs/specification/automation-run-channel.md new file mode 100644 index 000000000..06324b0cc --- /dev/null +++ b/docs/specification/automation-run-channel.md @@ -0,0 +1,53 @@ +# Automation Run Channel + +The automation-run channel represents one task-level execution of an +automation. A local run commonly links one session; hosted authorities may link +multiple attempts or workers. + +## URI + +```text +ahp-automation-run:/ +``` + +## State + +`AutomationRunState` contains immutable automation/trigger provenance, +discriminated lifecycle, an ordered session catalogue, optional primary +session, artifacts, and allowed operations. + +Linked `ahp-session:` and `ahp-chat:` channels remain authoritative for +conversation, tool-call, input-request, changeset, and per-session state. + +## Lifecycle + +```text +pending -> running -> completed + -> blocked -> running + -> failed + -> cancelled +``` + +`blocked` summarizes a user input, confirmation, authentication, or +client-execution dependency. Detailed response routing remains in linked +session state. + +## Actions + +- `automationRun/lifecycleChanged` +- `automationRun/sessionSet` +- `automationRun/sessionRemoved` +- `automationRun/primarySessionChanged` +- `automationRun/artifactSet` +- `automationRun/artifactRemoved` +- `automationRun/cancelRequested` + +Only `cancelRequested` is client-dispatchable. It is a side-effect request; the +durable result arrives through `lifecycleChanged`. + +## Reconciliation + +The host persists a run before external side effects and records each session +URI before sending its first message. Retrying `runAutomation` with the same +request ID returns the existing run URI. + diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 006eff20f..3b8a513b2 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -81,6 +81,8 @@ The specification is organised around the **channels** that AHP exposes — each - **[Authentication](/specification/authentication)** — RFC 9728 / RFC 6750 authentication flow. - **[Root Channel](/specification/root-channel)** — `ahp-root://` — agents, terminals catalogue, host config, session catalogue events. - **[Session Channel](/specification/session-channel)** — `ahp-session:/` — per-session state: the `chats` catalog, default chat, active clients, customizations, changesets, and aggregated status. +- **[Automation Channel](/specification/automation-channel)** — `ahp-automation:/` — durable trigger-based session workflows and run summaries. +- **[Automation Run Channel](/specification/automation-run-channel)** — `ahp-automation-run:/` — task lifecycle, sessions, and artifacts. - **[Chat Channel](/specification/chat-channel)** — `ahp-chat:/` — per-chat conversation state: turns, streaming, tool calls, pending messages, and input requests. - **[Terminal Channel](/specification/terminal-channel)** — per-terminal pty state, data flow, claims, command detection. - **[Telemetry Channel](/specification/telemetry-channel)** — `ahp-otlp:` — OpenTelemetry logs, traces, and metrics emitted by the agent host. diff --git a/docs/specification/root-channel.md b/docs/specification/root-channel.md index 364517535..d8b774685 100644 --- a/docs/specification/root-channel.md +++ b/docs/specification/root-channel.md @@ -69,6 +69,9 @@ target channel instead. | `authenticate` | request | Bearer-token push for protected resources is connection-level. | | `resolveSessionConfig` | request | Pre-creation config resolution happens before any session channel exists. | | `sessionConfigCompletions` | request | Completes dynamic fields in pre-creation session config. | +| `listAutomations` | request | Fetches the paginated automation catalogue. | +| `listAutomationTriggerDefinitions` | request | Describes host-defined event triggers. | +| `previewAutomationSchedule` | request | Computes host-canonical future schedule occurrences. | ### Notifications (`params.channel = "ahp-root://"`) @@ -79,6 +82,9 @@ target channel instead. | `root/sessionRemoved` | server → client notification | Session catalogue entry removed. | | `root/sessionSummaryChanged` | server → client notification | Session catalogue entry mutated. | | `root/progress` | server → client notification | Generic progress for a long-running operation a client opted into (e.g. an SDK download). | +| `root/automationAdded` | server → client notification | Automation catalogue entry created. | +| `root/automationRemoved` | server → client notification | Automation catalogue entry removed. | +| `root/automationSummaryChanged` | server → client notification | Automation catalogue entry mutated. | | `unsubscribe` | client → server notification | Stop receiving root-channel messages. | | `dispatchAction` | client → server notification | Dispatch a root-scoped client action (currently `root/configChanged`). | diff --git a/schema/actions.schema.json b/schema/actions.schema.json index fffbb4603..ae49340ef 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -2087,6 +2087,190 @@ "changes" ] }, + "AutomationDefinitionChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/definitionChanged" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "revision": { + "type": "number" + }, + "modifiedAt": { + "type": "string" + }, + "nextRunAt": { + "type": "string" + } + }, + "required": [ + "type", + "definition", + "revision", + "modifiedAt" + ] + }, + "AutomationRunSummarySetAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runSummarySet" + }, + "run": { + "$ref": "#/$defs/AutomationRunSummary" + } + }, + "required": [ + "type", + "run" + ] + }, + "AutomationRunSummaryRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runSummaryRemoved" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "run" + ] + }, + "AutomationRunsLoadedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runsLoaded" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + } + }, + "nextCursor": { + "type": "string" + } + }, + "required": [ + "type", + "runs" + ] + }, + "AutomationRunLifecycleChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/lifecycleChanged" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + } + }, + "required": [ + "type", + "lifecycle", + "operations" + ] + }, + "AutomationRunSessionSetAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/sessionSet" + }, + "session": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "session" + ] + }, + "AutomationRunSessionRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/sessionRemoved" + }, + "session": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "session" + ] + }, + "AutomationRunPrimarySessionChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/primarySessionChanged" + }, + "primarySession": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type" + ] + }, + "AutomationRunArtifactSetAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/artifactSet" + }, + "artifact": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "required": [ + "type", + "artifact" + ] + }, + "AutomationRunArtifactRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/artifactRemoved" + }, + "artifactId": { + "type": "string" + } + }, + "required": [ + "type", + "artifactId" + ] + }, + "AutomationRunCancelRequestedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/cancelRequested" + } + }, + "required": [ + "type" + ] + }, "ChatToolCallConfirmedAction": { "oneOf": [ { @@ -2602,6 +2786,12 @@ }, { "$ref": "#/$defs/ChatState" + }, + { + "$ref": "#/$defs/AutomationState" + }, + { + "$ref": "#/$defs/AutomationRunState" } ], "description": "The current state of the resource" @@ -2829,6 +3019,25 @@ "values" ] }, + "AutomationSessionOrigin": { + "type": "object", + "properties": { + "kind": { + "const": "automation" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "kind", + "automation", + "run" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -2849,6 +3058,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -2891,6 +3104,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -3195,6 +3412,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -7074,75 +7295,761 @@ "type" ] }, - "StringOrMarkdown": { - "oneOf": [ - { - "type": "string" + "AutomationLocalTime": { + "type": "object", + "properties": { + "hour": { + "type": "number" }, - { - "type": "object", - "properties": { - "markdown": { - "type": "string" - } - }, - "required": [ - "markdown" - ] + "minute": { + "type": "number" } - ], - "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." + }, + "required": [ + "hour", + "minute" + ] }, - "JsonPrimitive": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" + "AutomationHourlySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "hourly" + } + }, + "required": [ + "kind" + ] + }, + "AutomationDailySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "daily" }, - { - "type": "boolean" + "time": { + "$ref": "#/$defs/AutomationLocalTime" }, - { - "type": "null" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." } - ], - "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "required": [ + "kind", + "time", + "timeZone" + ] }, - "SessionInputRequest": { - "oneOf": [ - { - "$ref": "#/$defs/SessionChatInputRequest" + "AutomationWeeklySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "weekly" }, - { - "$ref": "#/$defs/SessionToolConfirmationRequest" + "weekday": { + "$ref": "#/$defs/AutomationWeekday" }, - { - "$ref": "#/$defs/SessionToolClientExecutionRequest" + "time": { + "$ref": "#/$defs/AutomationLocalTime" }, - { - "$ref": "#/$defs/SessionToolAuthenticationRequest" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." } - ], - "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." + }, + "required": [ + "kind", + "weekday", + "time", + "timeZone" + ] }, - "ChildCustomizationType": { - "oneOf": [ - { - "const": "agent" + "AutomationCronSchedule": { + "type": "object", + "properties": { + "kind": { + "const": "cron" }, - { - "const": "skill" + "expression": { + "type": "string", + "description": "Standard five-field Unix cron expression." }, - { - "const": "prompt" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." + } + }, + "required": [ + "kind", + "expression", + "timeZone" + ] + }, + "AutomationScheduleTrigger": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable within the automation definition." }, - { - "const": "rule" + "kind": { + "const": "schedule" }, - { - "const": "hook" + "schedule": { + "$ref": "#/$defs/AutomationSchedule" + }, + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy" + } + }, + "required": [ + "id", + "kind", + "schedule" + ] + }, + "AutomationEventTrigger": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable within the automation definition." + }, + "kind": { + "const": "event" + }, + "type": { + "type": "string", + "description": "Stable host-defined trigger type." + }, + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selected event actions." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Schema-defined values. Unknown entries must survive round-trips." + } + }, + "required": [ + "id", + "kind", + "type", + "events" + ] + }, + "AutomationTriggerEventDefinition": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "title" + ] + }, + "AutomationTriggerDefinition": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTriggerEventDefinition" + } + }, + "configSchema": { + "$ref": "#/$defs/ConfigSchema" + } + }, + "required": [ + "type", + "title", + "events" + ] + }, + "AutomationSessionTemplate": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Absence means a workspace-less session." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values resolved through `resolveSessionConfig`." + } + } + }, + "AutomationDefinition": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "Initial user message sent to each new session." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate" + }, + "enabled": { + "type": "boolean", + "description": "Controls automatic triggers; manual runs remain permitted." + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + }, + "description": "Empty means manual-only." + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "title", + "message", + "session", + "enabled", + "triggers" + ] + }, + "AutomationRuntimeState": { + "type": "object", + "properties": { + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + } + }, + "AutomationSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "title": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "triggerCount": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "lastRun": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "revision": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "revision": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries." + }, + "runsNextCursor": { + "type": "string" + }, + "runtime": { + "$ref": "#/$defs/AutomationRuntimeState" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationRunBlocker": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind" + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "trigger" + }, + "triggerId": { + "type": "string" + }, + "scheduledFor": { + "type": "string" + }, + "catchUp": { + "type": "boolean" + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined event provenance containing no secrets." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "usage": { + "$ref": "#/$defs/UsageInfo" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "AutomationFailedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "failed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "error": { + "$ref": "#/$defs/ErrorInfo" + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "AutomationCancelledRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "cancelled" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "AutomationRunArtifact": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "AutomationRunSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "sessionCount": { + "type": "number" + }, + "artifactCount": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "AutomationRunState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "sessions": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "artifacts": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessions", + "artifacts", + "operations" + ] + }, + "StringOrMarkdown": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "markdown": { + "type": "string" + } + }, + "required": [ + "markdown" + ] + } + ], + "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." + }, + "JsonPrimitive": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "SessionOrigin": { + "$ref": "#/$defs/AutomationSessionOrigin" + }, + "SessionInputRequest": { + "oneOf": [ + { + "$ref": "#/$defs/SessionChatInputRequest" + }, + { + "$ref": "#/$defs/SessionToolConfirmationRequest" + }, + { + "$ref": "#/$defs/SessionToolClientExecutionRequest" + }, + { + "$ref": "#/$defs/SessionToolAuthenticationRequest" + } + ], + "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." + }, + "ChildCustomizationType": { + "oneOf": [ + { + "const": "agent" + }, + { + "const": "skill" + }, + { + "const": "prompt" + }, + { + "const": "rule" + }, + { + "const": "hook" }, { "const": "mcpServer" @@ -7505,6 +8412,64 @@ ], "description": "A content part within terminal output." }, + "AutomationSchedule": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationHourlySchedule" + }, + { + "$ref": "#/$defs/AutomationDailySchedule" + }, + { + "$ref": "#/$defs/AutomationWeeklySchedule" + }, + { + "$ref": "#/$defs/AutomationCronSchedule" + } + ] + }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$ref": "#/$defs/AutomationEventTrigger" + } + ] + }, + "AutomationRunCause": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ] + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ] + }, "StateAction": { "description": "Discriminated union of all state actions.", "oneOf": [ @@ -7765,6 +8730,39 @@ }, { "$ref": "#/$defs/ResourceWatchChangedAction" + }, + { + "$ref": "#/$defs/AutomationDefinitionChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunSummarySetAction" + }, + { + "$ref": "#/$defs/AutomationRunSummaryRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunsLoadedAction" + }, + { + "$ref": "#/$defs/AutomationRunLifecycleChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunSessionSetAction" + }, + { + "$ref": "#/$defs/AutomationRunSessionRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunPrimarySessionChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunArtifactSetAction" + }, + { + "$ref": "#/$defs/AutomationRunArtifactRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunCancelRequestedAction" } ] }, @@ -7838,6 +8836,12 @@ "type": "string", "description": "Execution lifecycle of a {@link ChangesetOperation}.\n\nAn operation is invoked imperatively via `invokeChangesetOperation`, but\nits progress and outcome are reflected back into changeset state so that\nevery subscriber observes a consistent view (e.g. a spinner on a \"Create\nPull Request\" button, or an inline error after a failed \"revert\")." }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string" + }, "PolicyState": { "enum": [ "enabled", @@ -7925,6 +8929,42 @@ ], "type": "string", "description": "Discriminant for {@link ResourceChange.type}." + }, + "AutomationWeekday": { + "enum": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "type": "string" + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string" + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "type": "string" + }, + "AutomationRunBlockerKind": { + "enum": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "type": "string" } } } diff --git a/schema/commands.schema.json b/schema/commands.schema.json index e65cb0f59..9094eecc2 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -172,6 +172,10 @@ "telemetry": { "$ref": "#/$defs/TelemetryCapabilities", "description": "OTLP telemetry channels the host emits, if any. Each populated field is\neither a literal `ahp-otlp:` channel URI or an RFC 6570 URI template a\nclient expands before subscribing (currently only the `logs` channel\ndefines a template variable, `{level}`, for subscriber-side severity\nfiltering). Clients MAY ignore signals they cannot process." + }, + "automations": { + "$ref": "#/$defs/AutomationCapabilities", + "description": "Host automation support. Absence means unsupported." } }, "required": [ @@ -180,6 +184,89 @@ "snapshots" ] }, + "AutomationCapabilities": { + "type": "object", + "properties": { + "execution": { + "$ref": "#/$defs/AutomationExecutionCapabilities" + }, + "create": { + "$ref": "#/$defs/AutomationCreateCapability" + }, + "schedules": { + "$ref": "#/$defs/AutomationScheduleCapabilities" + }, + "runCancellation": { + "$ref": "#/$defs/AutomationRunCancellationCapability" + }, + "schedulePreview": { + "$ref": "#/$defs/AutomationSchedulePreviewCapability" + }, + "runHistoryLimit": { + "type": "number" + } + }, + "required": [ + "execution" + ] + }, + "AutomationExecutionCapabilities": { + "type": "object", + "properties": { + "lifetime": { + "$ref": "#/$defs/AutomationExecutionLifetime" + } + }, + "required": [ + "lifetime" + ] + }, + "AutomationCreateCapability": { + "type": "object", + "properties": {} + }, + "AutomationScheduleCapabilities": { + "type": "object", + "properties": { + "kinds": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationScheduleKind" + } + }, + "cron": { + "$ref": "#/$defs/AutomationCronScheduleCapability" + } + }, + "required": [ + "kinds" + ] + }, + "AutomationCronScheduleCapability": { + "type": "object", + "properties": { + "dialect": { + "type": "string", + "enum": [ + "unix5" + ] + }, + "minIntervalMinutes": { + "type": "number" + } + }, + "required": [ + "dialect" + ] + }, + "AutomationRunCancellationCapability": { + "type": "object", + "properties": {} + }, + "AutomationSchedulePreviewCapability": { + "type": "object", + "properties": {} + }, "PingParams": { "type": "object", "description": "Verifies that the AHP connection is still alive and keeps it from being\nclosed by idle-timeout intermediaries (proxies, load balancers, etc.).\n\nThe server MUST respond regardless of whether the client has completed\n`initialize` or holds any subscriptions. Ping carries no payload in either\ndirection; the response itself is the signal.", @@ -1498,649 +1585,1285 @@ "channel" ] }, - "Icon": { + "ListAutomationsParams": { "type": "object", - "description": "An optionally-sized icon that can be displayed in a user interface.", "properties": { - "src": { - "$ref": "#/$defs/URI", - "description": "A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a\n`data:` URI with Base64-encoded image data.\n\nConsumers SHOULD take steps to ensure URLs serving icons are from the\nsame domain as the client/server or a trusted domain.\n\nConsumers SHOULD take appropriate precautions when consuming SVGs as they can contain\nexecutable JavaScript." - }, - "contentType": { + "channel": { "type": "string", - "description": "Optional MIME type override if the source MIME type is missing or generic.\nFor example: `\"image/png\"`, `\"image/jpeg\"`, or `\"image/svg+xml\"`." + "enum": [ + "ahp-root://" + ] }, - "sizes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Optional array of strings that specify sizes at which the icon can be used.\nEach string should be in WxH format (e.g., `\"48x48\"`, `\"96x96\"`) or `\"any\"` for scalable formats like SVG.\n\nIf not provided, the client should assume that the icon can be used at any size." + "limit": { + "type": "number", + "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." }, - "theme": { + "cursor": { "type": "string", - "enum": [ - "light", - "dark" - ], - "description": "Optional specifier for the theme this icon is designed for. `\"light\"` indicates\nthe icon is designed to be used with a light background, and `\"dark\"` indicates\nthe icon is designed to be used with a dark background.\n\nIf not provided, the client should assume the icon can be used with any theme." + "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + }, + "enabled": { + "type": "boolean" } }, "required": [ - "src" + "channel" ] }, - "ProtectedResourceMetadata": { + "ListAutomationsResult": { "type": "object", - "description": "Describes a protected resource's authentication requirements using\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) (OAuth 2.0\nProtected Resource Metadata) semantics.\n\nField names use snake_case to match the RFC 9728 JSON format.", "properties": { - "resource": { - "type": "string", - "description": "REQUIRED. The protected resource's resource identifier, a URL using the\n`https` scheme with no fragment component (e.g. `\"https://api.github.com\"`)." - }, - "resource_name": { - "type": "string", - "description": "OPTIONAL. Human-readable name of the protected resource." - }, - "authorization_servers": { - "type": "array", - "items": { - "type": "string" - }, - "description": "OPTIONAL. JSON array of OAuth authorization server identifier URLs." - }, - "jwks_uri": { + "nextCursor": { "type": "string", - "description": "OPTIONAL. URL of the protected resource's JWK Set document." - }, - "scopes_supported": { - "type": "array", - "items": { - "type": "string" - }, - "description": "RECOMMENDED. JSON array of OAuth 2.0 scope values used in authorization requests." - }, - "bearer_methods_supported": { - "type": "array", - "items": { - "type": "string" - }, - "description": "OPTIONAL. JSON array of Bearer Token presentation methods supported." + "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." }, - "resource_signing_alg_values_supported": { + "items": { "type": "array", "items": { - "type": "string" - }, - "description": "OPTIONAL. JSON array of JWS signing algorithms supported." - }, - "resource_documentation": { - "type": "string", - "description": "OPTIONAL. URL of human-readable documentation for the resource." - }, - "resource_policy_uri": { - "type": "string", - "description": "OPTIONAL. URL of the resource's data-usage policy." - }, - "resource_tos_uri": { - "type": "string", - "description": "OPTIONAL. URL of the resource's terms of service." - }, - "required": { - "type": "boolean", - "description": "AHP extension. Whether authentication is required for this resource.\n\n- `true` (default) — the agent cannot be used without a valid token.\n The server SHOULD return `AuthRequired` (`-32007`) if the client\n attempts to use the agent without authenticating.\n- `false` — the agent works without authentication but MAY offer\n enhanced capabilities when a token is provided.\n\nClients SHOULD treat an absent field the same as `true`." + "$ref": "#/$defs/AutomationSummary" + } } }, "required": [ - "resource" + "items" ] }, - "ConfigPropertySchema": { + "ListAutomationTriggerDefinitionsParams": { "type": "object", - "description": "A JSON Schema-compatible property descriptor with display extensions.\n\nStandard JSON Schema fields (`type`, `title`, `description`, `default`,\n`enum`) allow validators to process the schema. Display extensions\n(`enumLabels`, `enumDescriptions`) are parallel arrays that provide UI\nmetadata for each `enum` value.\n\nThis is the generic base type. See {@link SessionConfigPropertySchema} for\nsession-specific extensions.", "properties": { - "type": { + "channel": { "type": "string", "enum": [ - "string", - "number", - "boolean", - "array", - "object" - ], - "description": "JSON Schema: property type" - }, - "title": { - "type": "string", - "description": "JSON Schema: human-readable label for the property" - }, - "description": { - "type": "string", - "description": "JSON Schema: description / tooltip" - }, - "default": { - "description": "JSON Schema: default value" - }, - "enum": { - "type": "array", - "items": { - "$ref": "#/$defs/JsonPrimitive" - }, - "description": "JSON Schema: allowed values. May be primitives of any JSON type." + "ahp-root://" + ] }, - "enumLabels": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Display extension: human-readable label per enum value (parallel array)" + "provider": { + "type": "string" }, - "enumDescriptions": { + "workingDirectories": { "type": "array", "items": { - "type": "string" - }, - "description": "Display extension: description per enum value (parallel array)" - }, - "readOnly": { - "type": "boolean", - "description": "JSON Schema: when `true`, the property is displayed but cannot be modified by the user" - }, - "items": { - "$ref": "#/$defs/ConfigPropertySchema", - "description": "JSON Schema: schema for array items (used when `type` is `'array'`)" + "$ref": "#/$defs/URI" + } }, - "properties": { + "sessionConfig": { "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ConfigPropertySchema" - }, - "description": "JSON Schema: property descriptors for object properties (used when `type` is `'object'`)" - }, - "required": { - "type": "array", - "items": { - "type": "string" - }, - "description": "JSON Schema: list of required property ids (used when `type` is `'object'`)" - }, - "additionalProperties": { - "$ref": "#/$defs/ConfigPropertySchema", - "description": "JSON Schema: schema for additional properties not listed in `properties` (used when `type` is `'object'`)." + "additionalProperties": {} } }, "required": [ - "type", - "title" + "channel" ] }, - "ConfigSchema": { + "ListAutomationTriggerDefinitionsResult": { "type": "object", - "description": "A JSON Schema object describing available configuration properties.\n\nThis is the generic base type. See {@link SessionConfigSchema} for\nsession-specific usage.", "properties": { - "type": { - "type": "string", - "enum": [ - "object" - ], - "description": "JSON Schema: always `'object'`" - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ConfigPropertySchema" - }, - "description": "JSON Schema: property descriptors keyed by property id" - }, - "required": { + "items": { "type": "array", "items": { - "type": "string" - }, - "description": "JSON Schema: list of required property ids" + "$ref": "#/$defs/AutomationTriggerDefinition" + } } }, "required": [ - "type", - "properties" + "items" ] }, - "TextPosition": { + "CreateAutomationParams": { "type": "object", - "description": "A zero-based position within a textual document.", "properties": { - "line": { - "type": "number", - "description": "Zero-based line number." + "channel": { + "$ref": "#/$defs/URI" }, - "character": { - "type": "number", - "description": "Zero-based character offset within the line." - } - }, - "required": [ - "line", - "character" + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "import": { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "batchId": { + "type": "string" + }, + "itemId": { + "type": "string" + } + }, + "required": [ + "source", + "batchId", + "itemId" + ] + } + }, + "required": [ + "channel", + "definition" ] }, - "TextRange": { + "AutomationDefinitionPatch": { "type": "object", - "description": "A range within a textual document.", "properties": { - "start": { - "$ref": "#/$defs/TextPosition", - "description": "Start position of the range." + "title": { + "type": "string" }, - "end": { - "$ref": "#/$defs/TextPosition", - "description": "End position of the range." + "message": { + "$ref": "#/$defs/Message" + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate" + }, + "enabled": { + "type": "boolean" + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + } + }, + "UpdateAutomationParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI" + }, + "expectedRevision": { + "type": "number" + }, + "changes": { + "$ref": "#/$defs/AutomationDefinitionPatch" } }, "required": [ - "start", - "end" + "channel", + "expectedRevision", + "changes" ] }, - "TextSelection": { + "DisposeAutomationParams": { "type": "object", - "description": "A selection within a textual resource.\n\nThis is only meaningful for textual resources. Binary resources may still\nuse resource or embedded resource attachments, but they should not use this\ntext selection field.", "properties": { - "range": { - "$ref": "#/$defs/TextRange", - "description": "The range covered by the selection." + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." } }, "required": [ - "range" + "channel" ] }, - "ContentRef": { + "RunAutomationParams": { "type": "object", - "description": "A reference to large content stored outside the state tree.", "properties": { - "uri": { + "channel": { "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" - }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "description": "Channel URI this command targets." }, - "nonce": { - "type": "string", - "description": "Content nonce" + "requestId": { + "type": "string" } }, "required": [ - "uri" + "channel", + "requestId" ] }, - "FileEdit": { + "RunAutomationResult": { "type": "object", - "description": "Describes a file modification with before/after state and diff metadata.\n\nSupports creates (only `after`), deletes (only `before`), renames/moves\n(different `uri` in `before` and `after`), and edits (same `uri`, different content).", "properties": { - "before": { - "type": "object", - "properties": { - "uri": { - "$ref": "#/$defs/URI" - }, - "content": { - "$ref": "#/$defs/ContentRef" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." - }, - "after": { - "type": "object", - "properties": { - "uri": { - "$ref": "#/$defs/URI" - }, - "content": { - "$ref": "#/$defs/ContentRef" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." - }, - "diff": { - "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } - }, - "description": "Optional diff display metadata" + "run": { + "$ref": "#/$defs/URI" } - } + }, + "required": [ + "run" + ] }, - "UsageInfo": { + "FetchAutomationRunsParams": { "type": "object", "properties": { - "inputTokens": { - "type": "number", - "description": "Input tokens consumed" - }, - "outputTokens": { - "type": "number", - "description": "Output tokens generated" - }, - "model": { - "type": "string", - "description": "Model used" - }, - "cacheReadTokens": { - "type": "number", - "description": "Tokens read from cache" + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this usage report.\nClients MAY look for well-known optional keys here to provide enhanced UI." + "cursor": { + "type": "string" } - } + }, + "required": [ + "channel" + ] }, - "ErrorInfo": { + "FetchAutomationRunsResult": { + "type": "object", + "properties": {} + }, + "PreviewAutomationScheduleParams": { "type": "object", "properties": { - "errorType": { - "type": "string", - "description": "Error type identifier" - }, - "message": { + "channel": { "type": "string", - "description": "Human-readable error message" + "enum": [ + "ahp-root://" + ] }, - "stack": { - "type": "string", - "description": "Stack trace" + "schedule": { + "$ref": "#/$defs/AutomationSchedule" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this error.\nClients MAY look for well-known optional keys here to provide enhanced UI\n(e.g. a structured chat fetch error for richer, localized messaging)." + "count": { + "type": "number" } }, "required": [ - "errorType", - "message" + "channel", + "schedule" ] }, - "Snapshot": { + "PreviewAutomationScheduleResult": { "type": "object", - "description": "A point-in-time snapshot of a subscribed resource's state, returned by\n`initialize`, `reconnect`, and `subscribe`.", "properties": { - "resource": { - "$ref": "#/$defs/URI", - "description": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" - }, - "state": { - "oneOf": [ - { - "$ref": "#/$defs/RootState" - }, - { - "$ref": "#/$defs/SessionState" - }, - { - "$ref": "#/$defs/TerminalState" - }, - { - "$ref": "#/$defs/ChangesetState" - }, - { - "$ref": "#/$defs/ResourceWatchState" - }, - { - "$ref": "#/$defs/AnnotationsState" - }, - { - "$ref": "#/$defs/ChatState" - } - ], - "description": "The current state of the resource" - }, - "fromSeq": { - "type": "number", - "description": "The `serverSeq` at which this snapshot was taken. Subsequent actions will have `serverSeq > fromSeq`." + "items": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "resource", - "state", - "fromSeq" + "items" ] }, - "RootState": { + "Icon": { "type": "object", - "description": "Global state shared with every client subscribed to `ahp-root://`.", + "description": "An optionally-sized icon that can be displayed in a user interface.", "properties": { - "agents": { - "type": "array", - "items": { - "$ref": "#/$defs/AgentInfo" - }, - "description": "Available agent backends and their models" + "src": { + "$ref": "#/$defs/URI", + "description": "A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a\n`data:` URI with Base64-encoded image data.\n\nConsumers SHOULD take steps to ensure URLs serving icons are from the\nsame domain as the client/server or a trusted domain.\n\nConsumers SHOULD take appropriate precautions when consuming SVGs as they can contain\nexecutable JavaScript." }, - "activeSessions": { - "type": "number", - "description": "Number of active (non-disposed) sessions on the server" + "contentType": { + "type": "string", + "description": "Optional MIME type override if the source MIME type is missing or generic.\nFor example: `\"image/png\"`, `\"image/jpeg\"`, or `\"image/svg+xml\"`." }, - "terminals": { + "sizes": { "type": "array", "items": { - "$ref": "#/$defs/TerminalInfo" + "type": "string" }, - "description": "Known terminals on the server. Subscribe to individual terminal URIs for full state." - }, - "config": { - "$ref": "#/$defs/RootConfigState", - "description": "Agent host configuration schema and current values" + "description": "Optional array of strings that specify sizes at which the icon can be used.\nEach string should be in WxH format (e.g., `\"48x48\"`, `\"96x96\"`) or `\"any\"` for scalable formats like SVG.\n\nIf not provided, the client should assume that the icon can be used at any size." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata about the agent host itself.\n\nClients MAY look for well-known keys here to provide enhanced UI." + "theme": { + "type": "string", + "enum": [ + "light", + "dark" + ], + "description": "Optional specifier for the theme this icon is designed for. `\"light\"` indicates\nthe icon is designed to be used with a light background, and `\"dark\"` indicates\nthe icon is designed to be used with a dark background.\n\nIf not provided, the client should assume the icon can be used with any theme." } }, "required": [ - "agents" + "src" ] }, - "AgentInfo": { + "ProtectedResourceMetadata": { "type": "object", + "description": "Describes a protected resource's authentication requirements using\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) (OAuth 2.0\nProtected Resource Metadata) semantics.\n\nField names use snake_case to match the RFC 9728 JSON format.", "properties": { - "provider": { + "resource": { "type": "string", - "description": "Agent provider ID (e.g. `'copilot'`)" + "description": "REQUIRED. The protected resource's resource identifier, a URL using the\n`https` scheme with no fragment component (e.g. `\"https://api.github.com\"`)." }, - "displayName": { + "resource_name": { "type": "string", - "description": "Human-readable name" + "description": "OPTIONAL. Human-readable name of the protected resource." }, - "description": { + "authorization_servers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OPTIONAL. JSON array of OAuth authorization server identifier URLs." + }, + "jwks_uri": { "type": "string", - "description": "Description string" + "description": "OPTIONAL. URL of the protected resource's JWK Set document." }, - "models": { + "scopes_supported": { "type": "array", "items": { - "$ref": "#/$defs/SessionModelInfo" + "type": "string" }, - "description": "Available models for this agent" + "description": "RECOMMENDED. JSON array of OAuth 2.0 scope values used in authorization requests." }, - "protectedResources": { + "bearer_methods_supported": { "type": "array", "items": { - "$ref": "#/$defs/ProtectedResourceMetadata" + "type": "string" }, - "description": "Protected resources this agent requires authentication for.\n\nEach entry describes an OAuth 2.0 protected resource using\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) semantics.\nClients should obtain tokens from the declared `authorization_servers`\nand push them via the `authenticate` command before creating sessions\nwith this agent." + "description": "OPTIONAL. JSON array of Bearer Token presentation methods supported." }, - "customizations": { + "resource_signing_alg_values_supported": { "type": "array", "items": { - "$ref": "#/$defs/Customization" + "type": "string" }, - "description": "Customizations associated with this agent.\n\nEither container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with — or top-level\n{@link McpServerCustomization | `McpServerCustomization`} entries\nthe agent host declares directly. When a session is created with\nthis agent, these entries are augmented (e.g. directory URIs are\nresolved against the workspace, children are parsed) and propagated\ninto the session's `customizations` list." + "description": "OPTIONAL. JSON array of JWS signing algorithms supported." }, - "capabilities": { - "$ref": "#/$defs/AgentCapabilities", - "description": "Static capabilities the agent advertises about itself. Clients use these\nto gate features (multi-chat, fork) instead of switching on the provider\nid." - } - }, - "required": [ - "provider", - "displayName", - "description", - "models" - ] - }, - "AgentCapabilities": { - "type": "object", - "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", - "properties": { - "multipleChats": { - "$ref": "#/$defs/MultipleChatsCapability", - "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." + "resource_documentation": { + "type": "string", + "description": "OPTIONAL. URL of human-readable documentation for the resource." }, - "multipleWorkingDirectories": { - "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", - "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." - } - } - }, - "MultipleChatsCapability": { - "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", - "properties": { - "fork": { - "type": "boolean", - "description": "The agent can fork a chat from a specific turn. When absent or `false`,\nclients MUST NOT pass a {@link ChatSource} with `kind: \"fork\"` to\n`createChat`.\nForking always implies multi-chat support." + "resource_policy_uri": { + "type": "string", + "description": "OPTIONAL. URL of the resource's data-usage policy." }, - "sideChat": { - "type": "boolean", - "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." - } - } - }, - "MultipleWorkingDirectoriesCapability": { - "type": "object", - "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", - "properties": { - "immutablePrimary": { + "resource_tos_uri": { + "type": "string", + "description": "OPTIONAL. URL of the resource's terms of service." + }, + "required": { "type": "boolean", - "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + "description": "AHP extension. Whether authentication is required for this resource.\n\n- `true` (default) — the agent cannot be used without a valid token.\n The server SHOULD return `AuthRequired` (`-32007`) if the client\n attempts to use the agent without authenticating.\n- `false` — the agent works without authentication but MAY offer\n enhanced capabilities when a token is provided.\n\nClients SHOULD treat an absent field the same as `true`." } - } + }, + "required": [ + "resource" + ] }, - "SessionModelInfo": { + "ConfigPropertySchema": { "type": "object", + "description": "A JSON Schema-compatible property descriptor with display extensions.\n\nStandard JSON Schema fields (`type`, `title`, `description`, `default`,\n`enum`) allow validators to process the schema. Display extensions\n(`enumLabels`, `enumDescriptions`) are parallel arrays that provide UI\nmetadata for each `enum` value.\n\nThis is the generic base type. See {@link SessionConfigPropertySchema} for\nsession-specific extensions.", "properties": { - "id": { + "type": { "type": "string", - "description": "Model identifier" + "enum": [ + "string", + "number", + "boolean", + "array", + "object" + ], + "description": "JSON Schema: property type" }, - "provider": { + "title": { "type": "string", - "description": "Provider this model belongs to" + "description": "JSON Schema: human-readable label for the property" }, - "name": { + "description": { "type": "string", - "description": "Human-readable model name" + "description": "JSON Schema: description / tooltip" }, - "maxContextWindow": { - "type": "number", - "description": "Maximum context window size" + "default": { + "description": "JSON Schema: default value" }, - "maxOutputTokens": { - "type": "number", - "description": "Maximum number of output tokens the model can generate" + "enum": { + "type": "array", + "items": { + "$ref": "#/$defs/JsonPrimitive" + }, + "description": "JSON Schema: allowed values. May be primitives of any JSON type." }, - "maxPromptTokens": { - "type": "number", - "description": "Maximum number of prompt (input) tokens the model accepts" + "enumLabels": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Display extension: human-readable label per enum value (parallel array)" }, - "supportsVision": { - "type": "boolean", - "description": "Whether the model supports vision" + "enumDescriptions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Display extension: description per enum value (parallel array)" }, - "policyState": { - "$ref": "#/$defs/PolicyState", - "description": "Policy configuration state" + "readOnly": { + "type": "boolean", + "description": "JSON Schema: when `true`, the property is displayed but cannot be modified by the user" }, - "configSchema": { - "$ref": "#/$defs/ConfigSchema", - "description": "Configuration schema describing model-specific options (e.g. thinking\nlevel). Clients present this as a form and pass the resolved values in\n{@link ModelSelection.config} when creating or changing sessions." + "items": { + "$ref": "#/$defs/ConfigPropertySchema", + "description": "JSON Schema: schema for array items (used when `type` is `'array'`)" }, - "_meta": { + "properties": { "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this model.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `pricing` key may carry model pricing metadata." + "additionalProperties": { + "$ref": "#/$defs/ConfigPropertySchema" + }, + "description": "JSON Schema: property descriptors for object properties (used when `type` is `'object'`)" + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "JSON Schema: list of required property ids (used when `type` is `'object'`)" + }, + "additionalProperties": { + "$ref": "#/$defs/ConfigPropertySchema", + "description": "JSON Schema: schema for additional properties not listed in `properties` (used when `type` is `'object'`)." } }, "required": [ - "id", - "provider", - "name" + "type", + "title" ] }, - "ModelSelection": { + "ConfigSchema": { "type": "object", - "description": "A model selection: the chosen model ID together with any model-specific\nconfiguration values whose keys correspond to the model's\n{@link SessionModelInfo.configSchema}.", + "description": "A JSON Schema object describing available configuration properties.\n\nThis is the generic base type. See {@link SessionConfigSchema} for\nsession-specific usage.", "properties": { - "id": { + "type": { "type": "string", - "description": "Model identifier" + "enum": [ + "object" + ], + "description": "JSON Schema: always `'object'`" }, - "config": { + "properties": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/JsonPrimitive" + "$ref": "#/$defs/ConfigPropertySchema" }, - "description": "Model-specific configuration values. Values are JSON primitives: most\npickers produce strings, but some (e.g. a numeric context-size picker)\nproduce numbers or booleans, which are carried through as-is." + "description": "JSON Schema: property descriptors keyed by property id" + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "description": "JSON Schema: list of required property ids" } }, "required": [ - "id" + "type", + "properties" ] }, - "RootConfigState": { + "TextPosition": { "type": "object", - "description": "Live agent-host configuration metadata.\n\nThe schema describes the available configuration properties and the values\ncontain the current value for each resolved property.", + "description": "A zero-based position within a textual document.", "properties": { - "schema": { - "$ref": "#/$defs/ConfigSchema", - "description": "JSON Schema describing available configuration properties" + "line": { + "type": "number", + "description": "Zero-based line number." }, - "values": { - "type": "object", - "additionalProperties": {}, - "description": "Current configuration values" + "character": { + "type": "number", + "description": "Zero-based character offset within the line." } }, "required": [ - "schema", + "line", + "character" + ] + }, + "TextRange": { + "type": "object", + "description": "A range within a textual document.", + "properties": { + "start": { + "$ref": "#/$defs/TextPosition", + "description": "Start position of the range." + }, + "end": { + "$ref": "#/$defs/TextPosition", + "description": "End position of the range." + } + }, + "required": [ + "start", + "end" + ] + }, + "TextSelection": { + "type": "object", + "description": "A selection within a textual resource.\n\nThis is only meaningful for textual resources. Binary resources may still\nuse resource or embedded resource attachments, but they should not use this\ntext selection field.", + "properties": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "The range covered by the selection." + } + }, + "required": [ + "range" + ] + }, + "ContentRef": { + "type": "object", + "description": "A reference to large content stored outside the state tree.", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + } + }, + "required": [ + "uri" + ] + }, + "FileEdit": { + "type": "object", + "description": "Describes a file modification with before/after state and diff metadata.\n\nSupports creates (only `after`), deletes (only `before`), renames/moves\n(different `uri` in `before` and `after`), and edits (same `uri`, different content).", + "properties": { + "before": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI" + }, + "content": { + "$ref": "#/$defs/ContentRef" + } + }, + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." + }, + "after": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI" + }, + "content": { + "$ref": "#/$defs/ContentRef" + } + }, + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." + }, + "diff": { + "type": "object", + "properties": { + "added": { + "type": "number" + }, + "removed": { + "type": "number" + } + }, + "description": "Optional diff display metadata" + } + } + }, + "UsageInfo": { + "type": "object", + "properties": { + "inputTokens": { + "type": "number", + "description": "Input tokens consumed" + }, + "outputTokens": { + "type": "number", + "description": "Output tokens generated" + }, + "model": { + "type": "string", + "description": "Model used" + }, + "cacheReadTokens": { + "type": "number", + "description": "Tokens read from cache" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this usage report.\nClients MAY look for well-known optional keys here to provide enhanced UI." + } + } + }, + "ErrorInfo": { + "type": "object", + "properties": { + "errorType": { + "type": "string", + "description": "Error type identifier" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "stack": { + "type": "string", + "description": "Stack trace" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this error.\nClients MAY look for well-known optional keys here to provide enhanced UI\n(e.g. a structured chat fetch error for richer, localized messaging)." + } + }, + "required": [ + "errorType", + "message" + ] + }, + "Snapshot": { + "type": "object", + "description": "A point-in-time snapshot of a subscribed resource's state, returned by\n`initialize`, `reconnect`, and `subscribe`.", + "properties": { + "resource": { + "$ref": "#/$defs/URI", + "description": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" + }, + "state": { + "oneOf": [ + { + "$ref": "#/$defs/RootState" + }, + { + "$ref": "#/$defs/SessionState" + }, + { + "$ref": "#/$defs/TerminalState" + }, + { + "$ref": "#/$defs/ChangesetState" + }, + { + "$ref": "#/$defs/ResourceWatchState" + }, + { + "$ref": "#/$defs/AnnotationsState" + }, + { + "$ref": "#/$defs/ChatState" + }, + { + "$ref": "#/$defs/AutomationState" + }, + { + "$ref": "#/$defs/AutomationRunState" + } + ], + "description": "The current state of the resource" + }, + "fromSeq": { + "type": "number", + "description": "The `serverSeq` at which this snapshot was taken. Subsequent actions will have `serverSeq > fromSeq`." + } + }, + "required": [ + "resource", + "state", + "fromSeq" + ] + }, + "RootState": { + "type": "object", + "description": "Global state shared with every client subscribed to `ahp-root://`.", + "properties": { + "agents": { + "type": "array", + "items": { + "$ref": "#/$defs/AgentInfo" + }, + "description": "Available agent backends and their models" + }, + "activeSessions": { + "type": "number", + "description": "Number of active (non-disposed) sessions on the server" + }, + "terminals": { + "type": "array", + "items": { + "$ref": "#/$defs/TerminalInfo" + }, + "description": "Known terminals on the server. Subscribe to individual terminal URIs for full state." + }, + "config": { + "$ref": "#/$defs/RootConfigState", + "description": "Agent host configuration schema and current values" + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata about the agent host itself.\n\nClients MAY look for well-known keys here to provide enhanced UI." + } + }, + "required": [ + "agents" + ] + }, + "AgentInfo": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Agent provider ID (e.g. `'copilot'`)" + }, + "displayName": { + "type": "string", + "description": "Human-readable name" + }, + "description": { + "type": "string", + "description": "Description string" + }, + "models": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionModelInfo" + }, + "description": "Available models for this agent" + }, + "protectedResources": { + "type": "array", + "items": { + "$ref": "#/$defs/ProtectedResourceMetadata" + }, + "description": "Protected resources this agent requires authentication for.\n\nEach entry describes an OAuth 2.0 protected resource using\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) semantics.\nClients should obtain tokens from the declared `authorization_servers`\nand push them via the `authenticate` command before creating sessions\nwith this agent." + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/Customization" + }, + "description": "Customizations associated with this agent.\n\nEither container customizations —\n{@link PluginCustomization | `PluginCustomization`} entries the agent\nbundles, plus {@link DirectoryCustomization | `DirectoryCustomization`}\nentries it watches in any workspace it's used with — or top-level\n{@link McpServerCustomization | `McpServerCustomization`} entries\nthe agent host declares directly. When a session is created with\nthis agent, these entries are augmented (e.g. directory URIs are\nresolved against the workspace, children are parsed) and propagated\ninto the session's `customizations` list." + }, + "capabilities": { + "$ref": "#/$defs/AgentCapabilities", + "description": "Static capabilities the agent advertises about itself. Clients use these\nto gate features (multi-chat, fork) instead of switching on the provider\nid." + } + }, + "required": [ + "provider", + "displayName", + "description", + "models" + ] + }, + "AgentCapabilities": { + "type": "object", + "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", + "properties": { + "multipleChats": { + "$ref": "#/$defs/MultipleChatsCapability", + "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." + }, + "multipleWorkingDirectories": { + "$ref": "#/$defs/MultipleWorkingDirectoriesCapability", + "description": "The session's agent can be granted tool access to more than one working\ndirectory. The directories are treated as equal peers except where the\nagent advertises {@link MultipleWorkingDirectoriesCapability.immutablePrimary}\n(some backends pin their first directory as a fixed process root).\n\nWhen absent, clients MUST NOT mutate a session's or chat's working-directory\nset and MUST NOT set more than one entry in\n{@link CreateSessionParams.workingDirectories}." + } + } + }, + "MultipleChatsCapability": { + "type": "object", + "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", + "properties": { + "fork": { + "type": "boolean", + "description": "The agent can fork a chat from a specific turn. When absent or `false`,\nclients MUST NOT pass a {@link ChatSource} with `kind: \"fork\"` to\n`createChat`.\nForking always implies multi-chat support." + }, + "sideChat": { + "type": "boolean", + "description": "The agent can create a side chat from a specific turn. When absent or\n`false`, clients MUST NOT pass a {@link ChatSource} with\n`kind: \"sideChat\"` to `createChat`.\n\nA side chat receives the source turn as context without copying the source\ntranscript into its own visible history. The source is identified by a\nstable `turnId`, which the host resolves against the source chat's current\n`activeTurn` or retained history. When it names the current active turn,\nthe host snapshots the available partial assistant response at creation\ntime. Side-chat support always implies multi-chat support." + } + } + }, + "MultipleWorkingDirectoriesCapability": { + "type": "object", + "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", + "properties": { + "immutablePrimary": { + "type": "boolean", + "description": "The agent's **first** working directory (index `0` of\n{@link CreateSessionParams.workingDirectories}) is an immutable primary:\nit is fixed for the lifetime of the session — clients MUST NOT remove or\nreorder it. Additional directories after it remain equal peers that can be\nadded and removed freely.\n\nAdvertised by backends whose agent process is rooted at a single directory\nthat cannot change once the session has started (e.g. the SDK's primary\n`workingDirectory`). When absent or `false`, all directories are equal\npeers and any of them may be removed." + } + } + }, + "SessionModelInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Model identifier" + }, + "provider": { + "type": "string", + "description": "Provider this model belongs to" + }, + "name": { + "type": "string", + "description": "Human-readable model name" + }, + "maxContextWindow": { + "type": "number", + "description": "Maximum context window size" + }, + "maxOutputTokens": { + "type": "number", + "description": "Maximum number of output tokens the model can generate" + }, + "maxPromptTokens": { + "type": "number", + "description": "Maximum number of prompt (input) tokens the model accepts" + }, + "supportsVision": { + "type": "boolean", + "description": "Whether the model supports vision" + }, + "policyState": { + "$ref": "#/$defs/PolicyState", + "description": "Policy configuration state" + }, + "configSchema": { + "$ref": "#/$defs/ConfigSchema", + "description": "Configuration schema describing model-specific options (e.g. thinking\nlevel). Clients present this as a form and pass the resolved values in\n{@link ModelSelection.config} when creating or changing sessions." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this model.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `pricing` key may carry model pricing metadata." + } + }, + "required": [ + "id", + "provider", + "name" + ] + }, + "ModelSelection": { + "type": "object", + "description": "A model selection: the chosen model ID together with any model-specific\nconfiguration values whose keys correspond to the model's\n{@link SessionModelInfo.configSchema}.", + "properties": { + "id": { + "type": "string", + "description": "Model identifier" + }, + "config": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/JsonPrimitive" + }, + "description": "Model-specific configuration values. Values are JSON primitives: most\npickers produce strings, but some (e.g. a numeric context-size picker)\nproduce numbers or booleans, which are carried through as-is." + } + }, + "required": [ + "id" + ] + }, + "RootConfigState": { + "type": "object", + "description": "Live agent-host configuration metadata.\n\nThe schema describes the available configuration properties and the values\ncontain the current value for each resolved property.", + "properties": { + "schema": { + "$ref": "#/$defs/ConfigSchema", + "description": "JSON Schema describing available configuration properties" + }, + "values": { + "type": "object", + "additionalProperties": {}, + "description": "Current configuration values" + } + }, + "required": [ + "schema", "values" ] }, - "SessionMetadata": { + "AutomationSessionOrigin": { + "type": "object", + "properties": { + "kind": { + "const": "automation" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "kind", + "automation", + "run" + ] + }, + "SessionMetadata": { + "type": "object", + "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", + "properties": { + "provider": { + "type": "string", + "description": "Agent provider ID" + }, + "title": { + "type": "string", + "description": "Session title" + }, + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current session status" + }, + "activity": { + "type": "string", + "description": "Human-readable description of what the session is currently doing" + }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, + "project": { + "$ref": "#/$defs/ProjectInfo", + "description": "Server-owned project for this session" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + }, + "annotations": { + "$ref": "#/$defs/AnnotationsSummary", + "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." + } + }, + "required": [ + "provider", + "title", + "status" + ] + }, + "SessionState": { + "type": "object", + "description": "Full state for a single session, loaded when a client subscribes to the session's URI.\n\nInlines (denormalizes) every {@link SessionMetadata} field directly onto\nitself so subscribers receive one flat object instead of a nested summary.\nThe lightweight catalog representation is {@link SessionSummary}, surfaced on\nthe root channel; the host keeps the two in sync via\n`root/sessionSummaryChanged`.", + "properties": { + "provider": { + "type": "string", + "description": "Agent provider ID" + }, + "title": { + "type": "string", + "description": "Session title" + }, + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current session status" + }, + "activity": { + "type": "string", + "description": "Human-readable description of what the session is currently doing" + }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, + "project": { + "$ref": "#/$defs/ProjectInfo", + "description": "Server-owned project for this session" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + }, + "annotations": { + "$ref": "#/$defs/AnnotationsSummary", + "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." + }, + "lifecycle": { + "$ref": "#/$defs/SessionLifecycle", + "description": "Session initialization state" + }, + "creationError": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details if creation failed" + }, + "serverTools": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolDefinition" + }, + "description": "Tools provided by the server (agent host) for this session" + }, + "activeClients": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionActiveClient" + }, + "description": "The clients currently providing tools and interactive capabilities to this\nsession. If multiple tools or customizations are provided by the same\nactive client, an agent host MAY deduplicate them when exposed to a model,\nwith a preference given to the client that started the turn.\n\nMembership is host-managed: clients add (or refresh) themselves with\n`session/activeClientSet`, and the host removes them with\n`session/activeClientRemoved` when they unsubscribe, disconnect without\nreconnecting in time, or reconnect without resubscribing to the session." + }, + "chats": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatSummary" + }, + "description": "Catalog of chats in this session." + }, + "defaultChat": { + "$ref": "#/$defs/URI", + "description": "The chat that receives input when the user addresses the session without\nselecting a specific chat. This is a UI routing hint, not a hierarchy\nmarker — chats remain equal peers at the protocol level. Hosts MAY change\nthis over the session's lifetime." + }, + "config": { + "$ref": "#/$defs/SessionConfigState", + "description": "Session configuration schema and current values" + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/Customization" + }, + "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClients[].customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." + }, + "changesets": { + "type": "array", + "items": { + "$ref": "#/$defs/Changeset" + }, + "description": "Catalogue of changesets the server can produce for this session. Each\nentry advertises a subscribable view of file changes (uncommitted,\nsession-wide, per-turn, etc.) and the URI template the client expands\nbefore subscribing. See {@link Changeset} for the full shape and\n{@link /guide/changesets | Changesets} for an overview of the model." + }, + "inputNeeded": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionInputRequest" + }, + "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A list\nholding any entry other than\n{@link SessionInputRequestKind.ToolClientExecution} implies\n{@link SessionStatus.InputNeeded} on {@link SessionSummary.status};\nclient-execution entries are work delegated to a client rather than a\nprompt, so they leave the session's activity unchanged.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworking directories." + } + }, + "required": [ + "provider", + "title", + "status", + "lifecycle", + "activeClients", + "chats" + ] + }, + "SessionActiveClient": { "type": "object", - "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", + "description": "A client currently providing tools and interactive capabilities to a session.\n\nA session MAY have several active clients at once; entries in\n{@link SessionState.activeClients} are keyed by `clientId`. The server SHOULD\nautomatically remove an active client when that client disconnects.", + "properties": { + "clientId": { + "type": "string", + "description": "Client identifier (matches `clientId` from `initialize`)" + }, + "displayName": { + "type": "string", + "description": "Human-readable client name (e.g. `\"VS Code\"`)" + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolDefinition" + }, + "description": "Tools this client provides to the session" + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientPluginCustomization" + }, + "description": "Plugin customizations this client contributes to the session.\n\nClients publish in [Open Plugins](https://open-plugins.com/) format\n— i.e. always container-shaped plugins. They MAY synthesize virtual\nplugins in memory and rely on the host to expand them into concrete\nchildren inside {@link SessionState.customizations}." + } + }, + "required": [ + "clientId", + "tools" + ] + }, + "SessionInputRequestBase": { + "type": "object", + "description": "Fields common to every {@link SessionInputRequest} variant.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + } + }, + "required": [ + "id", + "chat" + ] + }, + "SessionChatInputRequest": { + "type": "object", + "description": "A user-input elicitation surfaced at the session level, mirroring the request\nfrom an unresolved {@link InputRequestResponsePart} in the owning chat.\n\nRespond by dispatching `chat/inputCompleted` (or syncing drafts with\n`chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},\nkeyed by {@link ChatInputRequest.id | `request.id`}.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "const": "chatInput" + }, + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The mirrored chat input request." + } + }, + "required": [ + "id", + "chat", + "kind", + "request" + ] + }, + "SessionToolConfirmationRequest": { + "type": "object", + "description": "A tool call blocked on confirmation — either parameter confirmation before\nexecution or result confirmation after — surfaced at the session level.\n\nRespond by dispatching `chat/toolCallConfirmed` (for\n{@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`\n(for {@link ToolCallPendingResultConfirmationState}) to\n{@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and\n`toolCall.toolCallId`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "const": "toolConfirmation" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallConfirmationState", + "description": "The tool call awaiting confirmation." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "toolCall" + ] + }, + "SessionToolClientExecutionRequest": { + "type": "object", + "description": "A running tool whose execution is delegated to an active client. Surfaced so\na client that provides the tool can pick up the work without subscribing to\nthe owning chat.\n\nThe {@link toolCall} is always a {@link ToolCallRunningState} (a\n{@link ToolCallState} in `running` status) whose\n{@link ToolCallRunningState.contributor | `contributor`} is a client\n{@link ToolCallClientContributor} whose `clientId` matches the denormalized\n{@link clientId} here. Execute and report the result by dispatching\n`chat/toolCallComplete` (and optionally streaming with\n`chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |\n`chat`}, keyed by `turnId` and `toolCall.toolCallId`.\n\nUnlike the other variants this does **not** raise\n{@link SessionStatus.InputNeeded}: the call has already cleared its\nconfirmation gate and is merely executing elsewhere, so the session stays\n{@link SessionStatus.InProgress} while it runs.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "const": "toolClientExecution" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "clientId": { + "type": "string", + "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "The running tool call the session wants the owning client to execute. The\nhost only ever populates this with a {@link ToolCallRunningState} (i.e. a\n{@link ToolCallState} in `running` status)." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "clientId", + "toolCall" + ] + }, + "SessionToolAuthenticationRequest": { + "type": "object", + "description": "A tool call blocked on MCP authentication mid-execution, surfaced at the\nsession level.\n\nThe {@link toolCall} is always a {@link ToolCallAuthRequiredState} (a\n{@link ToolCallState} in `auth-required` status). Unlike\n{@link SessionToolConfirmationRequest}, this is **not** answered by\ndispatching a `chat/*` action directly: the client obtains a token for\n{@link ToolCallAuthRequiredState.auth | `toolCall.auth`}`.resource` and\npushes it via the existing `authenticate` command (see\n{@link /specification/authentication | Authentication}). The host resumes\nthe tool call and dispatches `chat/toolCallAuthResolved` once the token is\naccepted, at which point it also removes this entry with\n`session/inputNeededRemoved`.", + "properties": { + "id": { + "type": "string", + "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + }, + "chat": { + "$ref": "#/$defs/URI", + "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + }, + "kind": { + "const": "toolAuthentication" + }, + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallAuthRequiredState", + "description": "The tool call awaiting authentication." + } + }, + "required": [ + "id", + "chat", + "kind", + "turnId", + "toolCall" + ] + }, + "ProjectInfo": { + "type": "object", + "description": "Server-owned project metadata for a session.", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Project URI" + }, + "displayName": { + "type": "string", + "description": "Human-readable project name" + } + }, + "required": [ + "uri", + "displayName" + ] + }, + "SessionSummary": { + "type": "object", + "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectories`: the session-level set. Individual chats MAY restrict\n to a subset via {@link ChatSummary.workingDirectories}; aggregating these\n up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", "properties": { "provider": { "type": "string", @@ -2158,6 +2881,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -2169,668 +2896,861 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." }, - "annotations": { - "$ref": "#/$defs/AnnotationsSummary", - "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." + "annotations": { + "$ref": "#/$defs/AnnotationsSummary", + "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "Session URI" + }, + "createdAt": { + "type": "string", + "description": "Creation timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "modifiedAt": { + "type": "string", + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + }, + "changes": { + "$ref": "#/$defs/ChangesSummary", + "description": "Aggregate summary of file changes associated with this session. Servers\nmay populate this to give clients a quick at-a-glance view of the\nsession's footprint (e.g., for list rendering) without requiring the\nclient to subscribe to a changeset." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Lightweight server-defined metadata clients may use for the session\npresentation. The protocol does not interpret these values; producers\nSHOULD keep the payload small because summaries appear in session lists\nand session notifications." } }, "required": [ "provider", "title", - "status" + "status", + "resource", + "createdAt", + "modifiedAt" ] }, - "SessionState": { + "ChangesSummary": { "type": "object", - "description": "Full state for a single session, loaded when a client subscribes to the session's URI.\n\nInlines (denormalizes) every {@link SessionMetadata} field directly onto\nitself so subscribers receive one flat object instead of a nested summary.\nThe lightweight catalog representation is {@link SessionSummary}, surfaced on\nthe root channel; the host keeps the two in sync via\n`root/sessionSummaryChanged`.", + "description": "Aggregate counts describing the file changes associated with a session.\n\nAll fields are optional so servers can populate only the metrics they\ncheaply have available.", "properties": { - "provider": { + "additions": { + "type": "number", + "description": "Total number of inserted lines across all changed files." + }, + "deletions": { + "type": "number", + "description": "Total number of deleted lines across all changed files." + }, + "files": { + "type": "number", + "description": "Number of files that have changes." + } + } + }, + "AgentSelection": { + "type": "object", + "description": "A selected custom agent for a session.\n\nThe `uri` identifies a specific custom agent (matching an\n{@link AgentCustomization.uri | `AgentCustomization.uri`} exposed via\nthe session's effective customizations). Consumers resolve the agent's\ndisplay name by looking up `uri` in the session's customization tree.\n\nA message with no `agent` selected uses the provider's default behavior.", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Stable agent URI (matches an {@link AgentCustomization.uri})." + } + }, + "required": [ + "uri" + ] + }, + "SessionConfigPropertySchema": { + "type": "object", + "description": "A session configuration property descriptor.\n\nExtends the generic {@link ConfigPropertySchema} with session-specific\ndisplay extensions.", + "properties": { + "type": { "type": "string", - "description": "Agent provider ID" + "enum": [ + "string", + "number", + "boolean", + "array", + "object" + ], + "description": "JSON Schema: property type" }, "title": { "type": "string", - "description": "Session title" - }, - "status": { - "$ref": "#/$defs/SessionStatus", - "description": "Current session status" + "description": "JSON Schema: human-readable label for the property" }, - "activity": { + "description": { "type": "string", - "description": "Human-readable description of what the session is currently doing" + "description": "JSON Schema: description / tooltip" }, - "project": { - "$ref": "#/$defs/ProjectInfo", - "description": "Server-owned project for this session" + "default": { + "description": "JSON Schema: default value" }, - "workingDirectories": { + "enum": { "type": "array", "items": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/JsonPrimitive" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." - }, - "annotations": { - "$ref": "#/$defs/AnnotationsSummary", - "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." - }, - "lifecycle": { - "$ref": "#/$defs/SessionLifecycle", - "description": "Session initialization state" - }, - "creationError": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details if creation failed" + "description": "JSON Schema: allowed values. May be primitives of any JSON type." }, - "serverTools": { + "enumLabels": { "type": "array", "items": { - "$ref": "#/$defs/ToolDefinition" + "type": "string" }, - "description": "Tools provided by the server (agent host) for this session" + "description": "Display extension: human-readable label per enum value (parallel array)" }, - "activeClients": { + "enumDescriptions": { "type": "array", "items": { - "$ref": "#/$defs/SessionActiveClient" + "type": "string" }, - "description": "The clients currently providing tools and interactive capabilities to this\nsession. If multiple tools or customizations are provided by the same\nactive client, an agent host MAY deduplicate them when exposed to a model,\nwith a preference given to the client that started the turn.\n\nMembership is host-managed: clients add (or refresh) themselves with\n`session/activeClientSet`, and the host removes them with\n`session/activeClientRemoved` when they unsubscribe, disconnect without\nreconnecting in time, or reconnect without resubscribing to the session." + "description": "Display extension: description per enum value (parallel array)" }, - "chats": { + "readOnly": { + "type": "boolean", + "description": "JSON Schema: when `true`, the property is displayed but cannot be modified by the user" + }, + "items": { + "$ref": "#/$defs/ConfigPropertySchema", + "description": "JSON Schema: schema for array items (used when `type` is `'array'`)" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ConfigPropertySchema" + }, + "description": "JSON Schema: property descriptors for object properties (used when `type` is `'object'`)" + }, + "required": { "type": "array", "items": { - "$ref": "#/$defs/ChatSummary" + "type": "string" }, - "description": "Catalog of chats in this session." + "description": "JSON Schema: list of required property ids (used when `type` is `'object'`)" }, - "defaultChat": { - "$ref": "#/$defs/URI", - "description": "The chat that receives input when the user addresses the session without\nselecting a specific chat. This is a UI routing hint, not a hierarchy\nmarker — chats remain equal peers at the protocol level. Hosts MAY change\nthis over the session's lifetime." + "additionalProperties": { + "$ref": "#/$defs/ConfigPropertySchema", + "description": "JSON Schema: schema for additional properties not listed in `properties` (used when `type` is `'object'`)." }, - "config": { - "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values" + "enumDynamic": { + "type": "boolean", + "description": "Display extension: when `true`, the full set of allowed values is too large\nto enumerate statically. The client SHOULD use `sessionConfigCompletions`\nto fetch matching values based on user input. Any values in `enum` are\nseed/recent values for initial display." }, - "customizations": { - "type": "array", - "items": { - "$ref": "#/$defs/Customization" + "sessionMutable": { + "type": "boolean", + "description": "When `true`, the user may change this property after session creation" + } + }, + "required": [ + "type", + "title" + ] + }, + "SessionConfigSchema": { + "type": "object", + "description": "A JSON Schema object describing available session configuration metadata.", + "properties": { + "type": { + "type": "string", + "enum": [ + "object" + ], + "description": "JSON Schema: always `'object'`" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/SessionConfigPropertySchema" }, - "description": "Top-level customizations active in this session.\n\nAlways one of the {@link Customization} variants:\n\n- Container customizations ({@link PluginCustomization},\n {@link DirectoryCustomization}) whose children — agents, skills,\n prompts, rules, hooks, MCP servers — live in each container's\n {@link ContainerCustomizationBase.children | `children`} array.\n- Top-level {@link McpServerCustomization} entries the host\n surfaces directly (for example a globally-configured MCP server\n that isn't bundled in a plugin or directory). MCP servers may\n also appear as children of a container.\n\nClient-published plugins arrive via\n{@link SessionActiveClient.customizations | `activeClients[].customizations`}\nand the host propagates them into this list (typically with the\ncontainer's `clientId` set and `children` populated). Clients\npublish in container shape only; bare MCP servers at the top level\nare server-originated." + "description": "JSON Schema: property descriptors keyed by property id" }, - "changesets": { + "required": { "type": "array", "items": { - "$ref": "#/$defs/Changeset" + "type": "string" + }, + "description": "JSON Schema: list of required property ids" + } + }, + "required": [ + "type", + "properties" + ] + }, + "SessionConfigState": { + "type": "object", + "description": "Live session configuration metadata.\n\nThe schema describes the available configuration properties and the values\ncontain the current value for each resolved property.", + "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "JSON Schema describing available configuration properties" + }, + "values": { + "type": "object", + "additionalProperties": {}, + "description": "Current configuration values" + } + }, + "required": [ + "schema", + "values" + ] + }, + "ToolDefinition": { + "type": "object", + "description": "Describes a tool available in a session, provided by either the server or the active client.", + "properties": { + "name": { + "type": "string", + "description": "Unique tool identifier" + }, + "title": { + "type": "string", + "description": "Human-readable display name" + }, + "description": { + "type": "string", + "description": "Description of what the tool does" + }, + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "object" + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type" + ], + "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + }, + "outputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "object" + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } }, - "description": "Catalogue of changesets the server can produce for this session. Each\nentry advertises a subscribable view of file changes (uncommitted,\nsession-wide, per-turn, etc.) and the URI template the client expands\nbefore subscribing. See {@link Changeset} for the full shape and\n{@link /guide/changesets | Changesets} for an overview of the model." + "required": [ + "type" + ], + "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." }, - "inputNeeded": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionInputRequest" - }, - "description": "Outstanding input the session is blocked on, aggregated across every chat\nso a client can discover and answer it from the session channel alone,\nwithout subscribing to individual chats.\n\nEach entry is self-sufficient: it carries the owning chat's URI plus every\nidentifier the client needs to respond. A client answers by dispatching the\nordinary `chat/*` action to that chat's channel — see\n{@link SessionInputRequest} for the per-variant response path. A list\nholding any entry other than\n{@link SessionInputRequestKind.ToolClientExecution} implies\n{@link SessionStatus.InputNeeded} on {@link SessionSummary.status};\nclient-execution entries are work delegated to a client rather than a\nprompt, so they leave the session's activity unchanged.\n\nHost-managed: the host upserts entries with `session/inputNeededSet` as\nchats raise requests and removes them with `session/inputNeededRemoved`\nonce the underlying request resolves." + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Behavioral hints about the tool. All properties are advisory." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this session.\n\nClients MAY look for well-known keys here to provide enhanced UI.\nFor example, a `git` key may provide extra git metadata about the session's\nworking directories." + "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "provider", - "title", - "status", - "lifecycle", - "activeClients", - "chats" + "name" ] }, - "SessionActiveClient": { + "ToolAnnotations": { "type": "object", - "description": "A client currently providing tools and interactive capabilities to a session.\n\nA session MAY have several active clients at once; entries in\n{@link SessionState.activeClients} are keyed by `clientId`. The server SHOULD\nautomatically remove an active client when that client disconnects.", + "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", "properties": { - "clientId": { + "title": { "type": "string", - "description": "Client identifier (matches `clientId` from `initialize`)" + "description": "Alternate human-readable title" }, - "displayName": { - "type": "string", - "description": "Human-readable client name (e.g. `\"VS Code\"`)" + "readOnlyHint": { + "type": "boolean", + "description": "Tool does not modify its environment (default: false)" }, - "tools": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolDefinition" - }, - "description": "Tools this client provides to the session" + "destructiveHint": { + "type": "boolean", + "description": "Tool may perform destructive updates (default: true)" }, - "customizations": { - "type": "array", - "items": { - "$ref": "#/$defs/ClientPluginCustomization" - }, - "description": "Plugin customizations this client contributes to the session.\n\nClients publish in [Open Plugins](https://open-plugins.com/) format\n— i.e. always container-shaped plugins. They MAY synthesize virtual\nplugins in memory and rely on the host to expand them into concrete\nchildren inside {@link SessionState.customizations}." + "idempotentHint": { + "type": "boolean", + "description": "Repeated calls with the same arguments have no additional effect (default: false)" + }, + "openWorldHint": { + "type": "boolean", + "description": "Tool may interact with external entities (default: true)" } - }, - "required": [ - "clientId", - "tools" - ] + } }, - "SessionInputRequestBase": { + "CustomizationBase": { "type": "object", - "description": "Fields common to every {@link SessionInputRequest} variant.", + "description": "Fields shared by every customization variant.", "properties": { "id": { "type": "string", - "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "chat": { + "uri": { "$ref": "#/$defs/URI", - "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." } }, "required": [ "id", - "chat" + "uri", + "name" ] }, - "SessionChatInputRequest": { + "CustomizationLoadingState": { "type": "object", - "description": "A user-input elicitation surfaced at the session level, mirroring the request\nfrom an unresolved {@link InputRequestResponsePart} in the owning chat.\n\nRespond by dispatching `chat/inputCompleted` (or syncing drafts with\n`chat/inputAnswerChanged`) to {@link SessionInputRequestBase.chat | `chat`},\nkeyed by {@link ChatInputRequest.id | `request.id`}.", + "description": "Container is being loaded by the host.", "properties": { - "id": { - "type": "string", - "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." - }, "kind": { - "const": "chatInput" - }, - "request": { - "$ref": "#/$defs/ChatInputRequest", - "description": "The mirrored chat input request." + "const": "loading" } }, "required": [ - "id", - "chat", - "kind", - "request" + "kind" ] }, - "SessionToolConfirmationRequest": { + "CustomizationLoadedState": { "type": "object", - "description": "A tool call blocked on confirmation — either parameter confirmation before\nexecution or result confirmation after — surfaced at the session level.\n\nRespond by dispatching `chat/toolCallConfirmed` (for\n{@link ToolCallPendingConfirmationState}) or `chat/toolCallResultConfirmed`\n(for {@link ToolCallPendingResultConfirmationState}) to\n{@link SessionInputRequestBase.chat | `chat`}, keyed by `turnId` and\n`toolCall.toolCallId`.", + "description": "Container loaded successfully.", "properties": { - "id": { - "type": "string", - "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." - }, "kind": { - "const": "toolConfirmation" - }, - "turnId": { - "type": "string", - "description": "The turn the tool call belongs to." - }, - "toolCall": { - "$ref": "#/$defs/ToolCallConfirmationState", - "description": "The tool call awaiting confirmation." + "const": "loaded" } }, "required": [ - "id", - "chat", - "kind", - "turnId", - "toolCall" + "kind" ] }, - "SessionToolClientExecutionRequest": { + "CustomizationDegradedState": { "type": "object", - "description": "A running tool whose execution is delegated to an active client. Surfaced so\na client that provides the tool can pick up the work without subscribing to\nthe owning chat.\n\nThe {@link toolCall} is always a {@link ToolCallRunningState} (a\n{@link ToolCallState} in `running` status) whose\n{@link ToolCallRunningState.contributor | `contributor`} is a client\n{@link ToolCallClientContributor} whose `clientId` matches the denormalized\n{@link clientId} here. Execute and report the result by dispatching\n`chat/toolCallComplete` (and optionally streaming with\n`chat/toolCallContentChanged`) to {@link SessionInputRequestBase.chat |\n`chat`}, keyed by `turnId` and `toolCall.toolCallId`.\n\nUnlike the other variants this does **not** raise\n{@link SessionStatus.InputNeeded}: the call has already cleared its\nconfirmation gate and is merely executing elsewhere, so the session stays\n{@link SessionStatus.InProgress} while it runs.", + "description": "Container partially loaded but has warnings.", "properties": { - "id": { - "type": "string", - "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." - }, "kind": { - "const": "toolClientExecution" - }, - "turnId": { - "type": "string", - "description": "The turn the tool call belongs to." + "const": "degraded" }, - "clientId": { + "message": { "type": "string", - "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." - }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "The running tool call the session wants the owning client to execute. The\nhost only ever populates this with a {@link ToolCallRunningState} (i.e. a\n{@link ToolCallState} in `running` status)." + "description": "Human-readable description of the warning." } }, "required": [ - "id", - "chat", "kind", - "turnId", - "clientId", - "toolCall" + "message" ] }, - "SessionToolAuthenticationRequest": { + "CustomizationErrorState": { "type": "object", - "description": "A tool call blocked on MCP authentication mid-execution, surfaced at the\nsession level.\n\nThe {@link toolCall} is always a {@link ToolCallAuthRequiredState} (a\n{@link ToolCallState} in `auth-required` status). Unlike\n{@link SessionToolConfirmationRequest}, this is **not** answered by\ndispatching a `chat/*` action directly: the client obtains a token for\n{@link ToolCallAuthRequiredState.auth | `toolCall.auth`}`.resource` and\npushes it via the existing `authenticate` command (see\n{@link /specification/authentication | Authentication}). The host resumes\nthe tool call and dispatches `chat/toolCallAuthResolved` once the token is\naccepted, at which point it also removes this entry with\n`session/inputNeededRemoved`.", + "description": "Container failed to load.", "properties": { - "id": { - "type": "string", - "description": "Stable key for this entry, unique within the session's\n{@link SessionState.inputNeeded} list. The host derives it however it likes\n(for example from the chat URI plus the underlying request or tool-call\nid); consumers MUST treat it as opaque. It is the key for the\n`session/inputNeededSet` / `session/inputNeededRemoved` upsert convention." - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "The chat the underlying request lives in. This is the channel a client\ndispatches its response to — it does not need to have subscribed to that\nchat first." - }, "kind": { - "const": "toolAuthentication" + "const": "error" }, - "turnId": { + "message": { "type": "string", - "description": "The turn the tool call belongs to." - }, - "toolCall": { - "$ref": "#/$defs/ToolCallAuthRequiredState", - "description": "The tool call awaiting authentication." + "description": "Human-readable error message." } }, "required": [ - "id", - "chat", "kind", - "turnId", - "toolCall" + "message" ] }, - "ProjectInfo": { + "ContainerCustomizationBase": { "type": "object", - "description": "Server-owned project metadata for a session.", + "description": "Fields shared by container customizations.", "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, "uri": { "$ref": "#/$defs/URI", - "description": "Project URI" + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "displayName": { - "type": "string", - "description": "Human-readable project name" + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." } }, "required": [ + "id", "uri", - "displayName" + "name", + "enabled" ] }, - "SessionSummary": { + "PluginCustomization": { "type": "object", - "description": "Lightweight catalog entry summarizing one session. Surfaced via\n{@link RootChannelCommands.listSessions | `root/listSessions`} and\n`root/sessionAdded`/`root/sessionSummaryChanged` notifications.\n\n**Aggregation across chats.** Once a session contains more than one chat,\nseveral `SessionSummary` fields are derived from the underlying\n{@link SessionState.chats | chat catalog}. Producers SHOULD follow these\nrules so clients that only consume the session summary (e.g. a session\nlist) still see meaningful state:\n\n- `status`: take the activity bits (`Idle` / `InProgress` / `InputNeeded` /\n `Error` — bits 0–4) from the\n {@link SessionState.defaultChat | default chat} when present, else from\n the most recently modified chat. **Promote** `InputNeeded` whenever any\n chat in the session needs input, and **promote** `Error` whenever any\n chat is in an error state — both override the default-chat bits. The\n orthogonal flag bits (`IsRead`, `IsArchived`) remain session-scoped.\n- `activity`: mirror the activity string of the default chat, or of the\n chat currently driving the promoted status bits when a non-default chat\n wins (e.g. the chat that raised `InputNeeded`).\n- `modifiedAt`: the max of all chats' `modifiedAt`.\n- `workingDirectories`: the session-level set. Individual chats MAY restrict\n to a subset via {@link ChatSummary.workingDirectories}; aggregating these\n up is meaningless and SHOULD NOT be attempted.\n- `changes`: optional roll-up across all chats. Producers MAY sum the\n per-chat changeset stats or report the most expensive chat's stats —\n whichever is cheaper for the host to compute.\n\nSessions with a single chat trivially satisfy all of the above (the chat's\nvalues pass through unchanged). The rules only matter once a session\ncarries multiple chats.", + "description": "An [Open Plugins](https://open-plugins.com/) plugin.", "properties": { - "provider": { - "type": "string", - "description": "Agent provider ID" - }, - "title": { + "id": { "type": "string", - "description": "Session title" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "status": { - "$ref": "#/$defs/SessionStatus", - "description": "Current session status" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "activity": { + "name": { "type": "string", - "description": "Human-readable description of what the session is currently doing" - }, - "project": { - "$ref": "#/$defs/ProjectInfo", - "description": "Server-owned project for this session" + "description": "Human-readable name." }, - "workingDirectories": { + "icons": { "type": "array", "items": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/Icon" }, - "description": "The working directories the session's agent has tool access to, as\nmaintained by the `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` actions. Directories are equal peers\nexcept when the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (the first\nentry is then a fixed process root). Individual chats MAY restrict to a\nsubset via {@link ChatSummary.workingDirectories | their own\n`workingDirectories`}; a chat that sets none operates against this full\nset." + "description": "Icons for UI display." }, - "annotations": { - "$ref": "#/$defs/AnnotationsSummary", - "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Session URI" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "createdAt": { - "type": "string", - "description": "Creation timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "modifiedAt": { + "clientId": { "type": "string", - "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "changes": { - "$ref": "#/$defs/ChangesSummary", - "description": "Aggregate summary of file changes associated with this session. Servers\nmay populate this to give clients a quick at-a-glance view of the\nsession's footprint (e.g., for list rendering) without requiring the\nclient to subscribe to a changeset." + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Lightweight server-defined metadata clients may use for the session\npresentation. The protocol does not interpret these values; producers\nSHOULD keep the payload small because summaries appear in session lists\nand session notifications." + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "const": "plugin" + }, + "version": { + "type": "string", + "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." } }, "required": [ - "provider", - "title", - "status", - "resource", - "createdAt", - "modifiedAt" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "ChangesSummary": { + "ClientPluginCustomization": { "type": "object", - "description": "Aggregate counts describing the file changes associated with a session.\n\nAll fields are optional so servers can populate only the metrics they\ncheaply have available.", + "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", "properties": { - "additions": { - "type": "number", - "description": "Total number of inserted lines across all changed files." - }, - "deletions": { - "type": "number", - "description": "Total number of deleted lines across all changed files." + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "files": { - "type": "number", - "description": "Number of files that have changes." - } - } - }, - "AgentSelection": { - "type": "object", - "description": "A selected custom agent for a session.\n\nThe `uri` identifies a specific custom agent (matching an\n{@link AgentCustomization.uri | `AgentCustomization.uri`} exposed via\nthe session's effective customizations). Consumers resolve the agent's\ndisplay name by looking up `uri` in the session's customization tree.\n\nA message with no `agent` selected uses the provider's default behavior.", - "properties": { "uri": { "$ref": "#/$defs/URI", - "description": "Stable agent URI (matches an {@link AgentCustomization.uri})." + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + }, + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/ChildCustomization" + }, + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + }, + "type": { + "const": "plugin" + }, + "version": { + "type": "string", + "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." + }, + "nonce": { + "type": "string", + "description": "Opaque version token used by the host to detect changes." } }, "required": [ - "uri" + "id", + "uri", + "name", + "enabled", + "type" ] }, - "SessionConfigPropertySchema": { + "DirectoryCustomization": { "type": "object", - "description": "A session configuration property descriptor.\n\nExtends the generic {@link ConfigPropertySchema} with session-specific\ndisplay extensions.", + "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { - "type": { + "id": { "type": "string", - "enum": [ - "string", - "number", - "boolean", - "array", - "object" - ], - "description": "JSON Schema: property type" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "title": { - "type": "string", - "description": "JSON Schema: human-readable label for the property" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "description": { + "name": { "type": "string", - "description": "JSON Schema: description / tooltip" - }, - "default": { - "description": "JSON Schema: default value" + "description": "Human-readable name." }, - "enum": { + "icons": { "type": "array", "items": { - "$ref": "#/$defs/JsonPrimitive" + "$ref": "#/$defs/Icon" }, - "description": "JSON Schema: allowed values. May be primitives of any JSON type." + "description": "Icons for UI display." }, - "enumLabels": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Display extension: human-readable label per enum value (parallel array)" + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, - "enumDescriptions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Display extension: description per enum value (parallel array)" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "readOnly": { + "enabled": { "type": "boolean", - "description": "JSON Schema: when `true`, the property is displayed but cannot be modified by the user" + "description": "Whether this container is currently enabled." }, - "items": { - "$ref": "#/$defs/ConfigPropertySchema", - "description": "JSON Schema: schema for array items (used when `type` is `'array'`)" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ConfigPropertySchema" - }, - "description": "JSON Schema: property descriptors for object properties (used when `type` is `'object'`)" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "required": { + "children": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChildCustomization" }, - "description": "JSON Schema: list of required property ids (used when `type` is `'object'`)" + "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." }, - "additionalProperties": { - "$ref": "#/$defs/ConfigPropertySchema", - "description": "JSON Schema: schema for additional properties not listed in `properties` (used when `type` is `'object'`)." + "type": { + "const": "directory" }, - "enumDynamic": { - "type": "boolean", - "description": "Display extension: when `true`, the full set of allowed values is too large\nto enumerate statically. The client SHOULD use `sessionConfigCompletions`\nto fetch matching values based on user input. Any values in `enum` are\nseed/recent values for initial display." + "contents": { + "$ref": "#/$defs/ChildCustomizationType", + "description": "Which child customization type this directory holds." }, - "sessionMutable": { + "writable": { "type": "boolean", - "description": "When `true`, the user may change this property after session creation" + "description": "Whether clients may write into this directory." } }, "required": [ + "id", + "uri", + "name", + "enabled", "type", - "title" + "contents", + "writable" ] }, - "SessionConfigSchema": { + "ChildCustomizationBase": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.", + "description": "Fields shared by the leaf child customizations that live inside a\ncontainer — {@link AgentCustomization}, {@link SkillCustomization},\n{@link PromptCustomization}, {@link RuleCustomization}, and\n{@link HookCustomization}.\n\n{@link McpServerCustomization} is also a child but does not extend this\nbase: it always carries an explicit {@link McpServerCustomization.enabled}\nbecause it can appear as a top-level customization too.", "properties": { - "type": { + "id": { "type": "string", - "enum": [ - "object" - ], - "description": "JSON Schema: always `'object'`" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/SessionConfigPropertySchema" - }, - "description": "JSON Schema: property descriptors keyed by property id" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "required": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "description": "JSON Schema: list of required property ids" - } - }, - "required": [ - "type", - "properties" - ] - }, - "SessionConfigState": { - "type": "object", - "description": "Live session configuration metadata.\n\nThe schema describes the available configuration properties and the values\ncontain the current value for each resolved property.", - "properties": { - "schema": { - "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing available configuration properties" + "description": "Icons for UI display." }, - "values": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "_meta": { "type": "object", "additionalProperties": {}, - "description": "Current configuration values" + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." } }, "required": [ - "schema", - "values" + "id", + "uri", + "name" ] }, - "ToolDefinition": { + "AgentCustomization": { "type": "object", - "description": "Describes a tool available in a session, provided by either the server or the active client.", + "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", "properties": { - "name": { + "id": { "type": "string", - "description": "Unique tool identifier" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "title": { - "type": "string", - "description": "Human-readable display name" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "description": { + "name": { "type": "string", - "description": "Description of what the tool does" - }, - "inputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "object" - ] - }, - "properties": { - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "type" - ], - "description": "JSON Schema defining the expected input parameters.\n\nOptional because client-provided tools may not have formal schemas.\nMirrors MCP `Tool.inputSchema`." + "description": "Human-readable name." }, - "outputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "object" - ] - }, - "properties": { - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + "description": "Icons for UI display." }, - "annotations": { - "$ref": "#/$defs/ToolAnnotations", - "description": "Behavioral hints about the tool. All properties are advisory." + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, + "type": { + "const": "agent" + }, + "description": { + "type": "string", + "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + }, + "model": { + "type": "string", + "description": "Model the agent is pinned to, sourced from the agent file's\nfrontmatter `model`. Absent means the agent inherits the session's\ndefault model." + }, + "tools": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Allowlist of tool names the agent is scoped to, sourced from the\nagent file's frontmatter `tools`. A non-empty list restricts the\nagent to exactly those tools. Absent — or an empty list — imposes no\nrestriction beyond the session default: the agent may use any\navailable tool. Producers express \"no restriction\" by omitting the\nfield rather than sending an empty array, so an empty list carries no\nmeaning distinct from absence." + }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, the agent will not auto-delegate to this custom agent\nas a sub-agent; it can only be selected by the user. Absent or\n`false` means the agent may delegate to it." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot select this custom agent (for example,\nin a picker); it remains available for the agent to auto-delegate\nto. Absent or `false` means the user may select it." } }, "required": [ - "name" + "id", + "uri", + "name", + "type" ] }, - "ToolAnnotations": { + "SkillCustomization": { "type": "object", - "description": "Behavioral hints about a tool. All properties are advisory and not\nguaranteed to faithfully describe tool behavior.\n\nMirrors MCP `ToolAnnotations` from the Model Context Protocol specification.", + "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", "properties": { - "title": { + "id": { "type": "string", - "description": "Alternate human-readable title" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "readOnlyHint": { - "type": "boolean", - "description": "Tool does not modify its environment (default: false)" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "destructiveHint": { + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + }, + "enabled": { "type": "boolean", - "description": "Tool may perform destructive updates (default: true)" + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, - "idempotentHint": { + "type": { + "const": "skill" + }, + "description": { + "type": "string", + "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." + }, + "disableModelInvocation": { "type": "boolean", - "description": "Repeated calls with the same arguments have no additional effect (default: false)" + "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." }, - "openWorldHint": { + "disableUserInvocation": { "type": "boolean", - "description": "Tool may interact with external entities (default: true)" + "description": "When `true`, the user cannot directly invoke this skill (for example,\nas a slash command); it remains available for the agent to\nauto-invoke. Absent or `false` means the user may invoke it." } - } + }, + "required": [ + "id", + "uri", + "name", + "type" + ] }, - "CustomizationBase": { + "PromptCustomization": { "type": "object", - "description": "Fields shared by every customization variant.", + "description": "A prompt contributed by a plugin or directory.", "properties": { "id": { "type": "string", @@ -2859,75 +3779,91 @@ "type": "object", "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, + "type": { + "const": "prompt" + }, + "description": { + "type": "string", + "description": "Short description of what the prompt does." } }, "required": [ "id", "uri", - "name" - ] - }, - "CustomizationLoadingState": { - "type": "object", - "description": "Container is being loaded by the host.", - "properties": { - "kind": { - "const": "loading" - } - }, - "required": [ - "kind" - ] - }, - "CustomizationLoadedState": { - "type": "object", - "description": "Container loaded successfully.", - "properties": { - "kind": { - "const": "loaded" - } - }, - "required": [ - "kind" + "name", + "type" ] }, - "CustomizationDegradedState": { + "RuleCustomization": { "type": "object", - "description": "Container partially loaded but has warnings.", + "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", "properties": { - "kind": { - "const": "degraded" + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "message": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { "type": "string", - "description": "Human-readable description of the warning." - } - }, - "required": [ - "kind", - "message" - ] - }, - "CustomizationErrorState": { - "type": "object", - "description": "Container failed to load.", - "properties": { - "kind": { - "const": "error" + "description": "Human-readable name." }, - "message": { + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, + "type": { + "const": "rule" + }, + "description": { "type": "string", - "description": "Human-readable error message." + "description": "Description of what the rule enforces." + }, + "alwaysApply": { + "type": "boolean", + "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." + }, + "globs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." } }, "required": [ - "kind", - "message" + "id", + "uri", + "name", + "type" ] }, - "ContainerCustomizationBase": { + "HookCustomization": { "type": "object", - "description": "Fields shared by container customizations.", + "description": "A hook manifest contributed by a plugin or directory.", "properties": { "id": { "type": "string", @@ -2959,34 +3895,22 @@ }, "enabled": { "type": "boolean", - "description": "Whether this container is currently enabled." - }, - "clientId": { - "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." + "type": { + "const": "hook" } }, "required": [ "id", "uri", "name", - "enabled" + "type" ] }, - "PluginCustomization": { + "McpServerCustomization": { "type": "object", - "description": "An [Open Plugins](https://open-plugins.com/) plugin.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { "id": { "type": "string", @@ -3016,3371 +3940,3511 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, + "type": { + "const": "mcpServer" + }, "enabled": { "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "Whether this MCP server is currently enabled." + }, + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." + }, + "channel": { + "$ref": "#/$defs/URI", + "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + }, + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + } + }, + "required": [ + "id", + "uri", + "name", + "type", + "enabled", + "state" + ] + }, + "McpServerCustomizationApps": { + "type": "object", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", + "properties": { + "capabilities": { + "$ref": "#/$defs/AhpMcpUiHostCapabilities", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." + } + }, + "required": [ + "capabilities" + ] + }, + "AhpMcpUiHostCapabilities": { + "type": "object", + "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "properties": { + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + }, + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." + }, + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." }, + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "object", + "additionalProperties": {} + } + }, + "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." + } + } + }, + "McpServerStartingState": { + "type": "object", + "description": "Server is registered with the host but has not yet started.", + "properties": { + "kind": { + "const": "starting" + } + }, + "required": [ + "kind" + ] + }, + "McpServerReadyState": { + "type": "object", + "description": "Server is running and serving requests.", + "properties": { + "kind": { + "const": "ready" + } + }, + "required": [ + "kind" + ] + }, + "McpOAuthClient": { + "type": "object", + "description": "A pre-registered OAuth client that clients use instead of dynamic client\nregistration when resolving an MCP authentication challenge.", + "properties": { "clientId": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." - }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." - }, - "children": { - "type": "array", - "items": { - "$ref": "#/$defs/ChildCustomization" - }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "const": "plugin" + "description": "OAuth client identifier registered with the authorization server." }, - "version": { + "clientSecret": { "type": "string", - "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." + "description": "OAuth client secret for a confidential client. Absence means the client is\npublic and uses a secretless flow such as authorization code with PKCE." } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "clientId" ] }, - "ClientPluginCustomization": { + "McpAuthRequirement": { "type": "object", - "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", + "description": "Reusable MCP authentication challenge — the RFC 9728 discovery info a\nclient needs to obtain a token and push it via the `authenticate` command.\nDeliberately carries **no token**: this describes what is being asked for,\nnever the bearer token itself.\n\nShared by two independent state machines that describe the same OAuth\nchallenge from different vantage points:\n\n- {@link McpServerAuthRequiredState} — the MCP server itself cannot serve\n *any* request until the client authenticates.\n- {@link ToolCallAuthRequiredState} — a specific in-flight tool call is\n paused pending authentication (typically\n {@link McpAuthRequiredReason.InsufficientScope} step-up auth\n mid-execution). The server state and the tool-call state remain\n separate on purpose: the server saying \"I need auth\" and a tool\n invocation saying \"I am waiting on that auth\" are different facts that\n can be true independently.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "oauthClient": { + "$ref": "#/$defs/McpOAuthClient", + "description": "Pre-registered OAuth client to use for authorization. When present, clients\nMUST use these credentials instead of dynamic client registration." }, - "name": { - "type": "string", - "description": "Human-readable name." + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." }, - "icons": { + "requiredScopes": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "type": "string" }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." - }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." }, - "clientId": { + "description": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Human-readable hint, typically from the OAuth `error_description`." + } + }, + "required": [ + "reason", + "resource" + ] + }, + "McpServerAuthRequiredState": { + "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "properties": { + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "oauthClient": { + "$ref": "#/$defs/McpOAuthClient", + "description": "Pre-registered OAuth client to use for authorization. When present, clients\nMUST use these credentials instead of dynamic client registration." }, - "children": { + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "type": "string" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "const": "plugin" + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." }, - "version": { + "description": { "type": "string", - "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." + "description": "Human-readable hint, typically from the OAuth `error_description`." }, - "nonce": { - "type": "string", - "description": "Opaque version token used by the host to detect changes." + "kind": { + "const": "authRequired" } }, "required": [ - "id", - "uri", - "name", - "enabled", - "type" + "reason", + "resource", + "kind" ] }, - "DirectoryCustomization": { + "McpServerErrorState": { "type": "object", - "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "kind": { + "const": "error" }, - "uri": { + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." + } + }, + "required": [ + "kind", + "error" + ] + }, + "McpServerStoppedState": { + "type": "object", + "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", + "properties": { + "kind": { + "const": "stopped" + } + }, + "required": [ + "kind" + ] + }, + "ChatState": { + "type": "object", + "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", + "properties": { + "resource": { "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Chat URI" }, - "name": { + "title": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "Chat title" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." + "activity": { + "type": "string", + "description": "Human-readable description of what the chat is currently doing" }, - "clientId": { + "modifiedAt": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" }, - "children": { + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." + }, + "workingDirectories": { "type": "array", "items": { - "$ref": "#/$defs/ChildCustomization" + "$ref": "#/$defs/URI" }, - "description": "Children discovered inside this container.\n\nAbsent means the host has not parsed this container yet. An empty\narray means the host parsed the container and it contributes\nnothing." - }, - "type": { - "const": "directory" - }, - "contents": { - "$ref": "#/$defs/ChildCustomizationType", - "description": "Which child customization type this directory holds." - }, - "writable": { - "type": "boolean", - "description": "Whether clients may write into this directory." - } - }, - "required": [ - "id", - "uri", - "name", - "enabled", - "type", - "contents", - "writable" - ] - }, - "ChildCustomizationBase": { - "type": "object", - "description": "Fields shared by the leaf child customizations that live inside a\ncontainer — {@link AgentCustomization}, {@link SkillCustomization},\n{@link PromptCustomization}, {@link RuleCustomization}, and\n{@link HookCustomization}.\n\n{@link McpServerCustomization} is also a child but does not extend this\nbase: it always carries an explicit {@link McpServerCustomization.enabled}\nbecause it can appear as a top-level customization too.", - "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "turns": { + "type": "array", + "items": { + "$ref": "#/$defs/Turn" + }, + "description": "Completed turns" }, - "name": { + "turnsNextCursor": { "type": "string", - "description": "Human-readable name." + "description": "Cursor for loading older completed turns into this chat state.\n\nPresence means `turns` is a tail window and more historical turns are\navailable. Pass this opaque cursor to `fetchTurns`; the host MUST insert\nthe loaded turns into state and update or clear this cursor before\nresponding. Absence means the state contains all retained turns." }, - "icons": { + "activeTurn": { + "$ref": "#/$defs/ActiveTurn", + "description": "Currently in-progress turn" + }, + "steeringMessage": { + "$ref": "#/$defs/PendingMessage", + "description": "Message to inject into the current turn at a convenient point" + }, + "queuedMessages": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/PendingMessage" }, - "description": "Icons for UI display." + "description": "Messages to send automatically as new turns after the current turn finishes" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "draft": { + "$ref": "#/$defs/Message", + "description": "The user's in-progress draft input for this chat — the message they are\ncomposing but have not sent yet, including its\n{@link Message.model | model} / {@link Message.agent | agent} selection\nand attachments.\n\nClients MAY periodically sync their local input state into this field so\na draft survives reloads and is visible to other clients viewing the same\nchat. Eager syncing is **not** required — clients SHOULD debounce and MAY\nsync only at convenient points. When presenting input UI for an existing\nchat, clients SHOULD use any `draft` to initialize their input state.\nCleared (set to `undefined`) once the message is sent." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." - }, - "enabled": { - "type": "boolean", - "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + "description": "Additional provider-specific metadata for this chat." } }, "required": [ - "id", - "uri", - "name" + "resource", + "title", + "status", + "modifiedAt", + "turns" ] }, - "AgentCustomization": { + "ChatSummary": { "type": "object", - "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", + "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { + "resource": { "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Chat URI" }, - "name": { + "title": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." - }, - "enabled": { - "type": "boolean", - "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + "description": "Chat title" }, - "type": { - "const": "agent" + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current chat status (reuses SessionStatus shape)" }, - "description": { + "activity": { "type": "string", - "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." + "description": "Human-readable description of what the chat is currently doing" }, - "model": { + "modifiedAt": { "type": "string", - "description": "Model the agent is pinned to, sourced from the agent file's\nfrontmatter `model`. Absent means the agent inherits the session's\ndefault model." + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "tools": { + "origin": { + "$ref": "#/$defs/ChatOrigin", + "description": "How this chat came into existence" + }, + "interactivity": { + "$ref": "#/$defs/ChatInteractivity", + "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." + }, + "workingDirectories": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/URI" }, - "description": "Allowlist of tool names the agent is scoped to, sourced from the\nagent file's frontmatter `tools`. A non-empty list restricts the\nagent to exactly those tools. Absent — or an empty list — imposes no\nrestriction beyond the session default: the agent may use any\navailable tool. Producers express \"no restriction\" by omitting the\nfield rather than sending an empty array, so an empty list carries no\nmeaning distinct from absence." - }, - "disableModelInvocation": { - "type": "boolean", - "description": "When `true`, the agent will not auto-delegate to this custom agent\nas a sub-agent; it can only be selected by the user. Absent or\n`false` means the agent may delegate to it." - }, - "disableUserInvocation": { - "type": "boolean", - "description": "When `true`, the user cannot select this custom agent (for example,\nin a picker); it remains available for the agent to auto-delegate\nto. Absent or `false` means the user may select it." + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "required": [ - "id", - "uri", - "name", - "type" + "resource", + "title", + "status", + "modifiedAt" ] }, - "SkillCustomization": { + "SideChatSelection": { "type": "object", - "description": "A skill contributed by a plugin or directory.\n\nCovers both [Open Plugins skill formats](https://open-plugins.com/agent-builders/components/skills)\n— the `skills/` directory layout (one subdirectory per skill, each with\na `SKILL.md`) and the flatter `commands/` directory of slash-command\nskills.", + "description": "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.", "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { + "text": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." - }, - "enabled": { - "type": "boolean", - "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." - }, - "type": { - "const": "skill" + "description": "Exact selected-text snapshot captured at `createChat` acceptance.\n\nMUST be non-empty." }, - "description": { + "responsePartId": { "type": "string", - "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." - }, - "disableModelInvocation": { - "type": "boolean", - "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." - }, - "disableUserInvocation": { - "type": "boolean", - "description": "When `true`, the user cannot directly invoke this skill (for example,\nas a slash command); it remains available for the agent to\nauto-invoke. Absent or `false` means the user may invoke it." + "description": "Optional provenance for the response part that contained {@link text} when\nthe host took the snapshot.\n\nAdvisory only: this is not a live range or offset and MUST NOT be used to\nrecompute `text`." } }, "required": [ - "id", - "uri", - "name", - "type" + "text" ] }, - "PromptCustomization": { + "PendingMessage": { "type": "object", - "description": "A prompt contributed by a plugin or directory.", + "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", "properties": { "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." - }, - "enabled": { - "type": "boolean", - "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." - }, - "type": { - "const": "prompt" + "description": "Unique identifier for this pending message" }, - "description": { - "type": "string", - "description": "Short description of what the prompt does." + "message": { + "$ref": "#/$defs/Message", + "description": "The message that will start the next turn" } }, "required": [ "id", - "uri", - "name", - "type" + "message" ] }, - "RuleCustomization": { - "type": "object", - "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", - "properties": { - "id": { - "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." - }, - "enabled": { - "type": "boolean", - "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + "ChatInputOption": { + "type": "object", + "description": "A choice in a select-style question.", + "properties": { + "id": { + "type": "string", + "description": "Stable option identifier; for MCP enum values this is the enum string" }, - "type": { - "const": "rule" + "label": { + "type": "string", + "description": "Display label" }, "description": { "type": "string", - "description": "Description of what the rule enforces." + "description": "Optional secondary text" }, - "alwaysApply": { + "recommended": { "type": "boolean", - "description": "When `true`, the rule is always active (subject to `globs` if any).\nWhen `false` or absent, the agent or user decides whether to apply\nthe rule." - }, - "globs": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." + "description": "Whether this option is the recommended/default choice" } }, "required": [ "id", - "uri", - "name", - "type" + "label" ] }, - "HookCustomization": { + "ChatInputQuestionBase": { "type": "object", - "description": "A hook manifest contributed by a plugin or directory.", "properties": { "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Stable question identifier used as the key in `answers`" }, - "name": { + "title": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "Short display title" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "enabled": { + "required": { "type": "boolean", - "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." - }, - "type": { - "const": "hook" + "description": "Whether the user must answer this question to accept the request" } }, "required": [ "id", - "uri", - "name", - "type" + "message" ] }, - "McpServerCustomization": { + "ChatInputTextQuestion": { "type": "object", - "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", + "description": "Text question within a chat input request.", "properties": { "id": { "type": "string", - "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." - }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + "description": "Stable question identifier used as the key in `answers`" }, - "name": { + "title": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "description": "Short display title" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "type": { - "const": "mcpServer" + "kind": { + "const": "text" }, - "enabled": { - "type": "boolean", - "description": "Whether this MCP server is currently enabled." + "format": { + "type": "string", + "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "Current lifecycle state of the MCP server." + "min": { + "type": "number", + "description": "Minimum string length" }, - "channel": { - "$ref": "#/$defs/URI", - "description": "An `mcp://`-protocol channel the client uses to side-channel traffic\ninto the upstream MCP server itself. The channel is NOT a fresh raw MCP\nconnection: it piggybacks on the AHP transport\nand skips the MCP `initialize` sequence.\n\nThe agent host MAY only serve a subset of MCP on this\nchannel; the served subset is described by domain-specific\ncapabilities such as those in\n{@link McpServerCustomizationApps.capabilities}.\n\nThe channel URI SHOULD be stable across the server's lifetime, but\nthe agent host MAY change it (for example across a restart) and\nMAY only expose it while the server is in\n{@link McpServerStatus.Ready | `Ready`}. Absence means no\nside-channel is currently available." + "max": { + "type": "number", + "description": "Maximum string length" }, - "mcpApp": { - "$ref": "#/$defs/McpServerCustomizationApps", - "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + "defaultValue": { + "type": "string", + "description": "Default text" } }, "required": [ "id", - "uri", - "name", - "type", - "enabled", - "state" - ] - }, - "McpServerCustomizationApps": { - "type": "object", - "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", - "properties": { - "capabilities": { - "$ref": "#/$defs/AhpMcpUiHostCapabilities", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nthe AHP host can satisfy for Views backed by this server. The\nclient feeds these straight through into the `hostCapabilities` of\nthe `ui/initialize` response delivered to the View." - } - }, - "required": [ - "capabilities" + "message", + "kind" ] }, - "AhpMcpUiHostCapabilities": { + "ChatInputNumberQuestion": { "type": "object", - "description": "The subset of MCP App\n[`HostCapabilities`](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)\nan AHP host can derive from the upstream MCP server (and from AHP's own\nforwarding plumbing). Advertised on\n{@link McpServerCustomizationApps.capabilities} so clients can pass it\nthrough into the `hostCapabilities` of the `ui/initialize` response\ndelivered to an MCP App View.\n\nField names mirror the MCP Apps spec exactly, so the AHP-side producer\ncan pass them straight through into the `hostCapabilities` of the\n`ui/initialize` response delivered to the View.\n\nCapabilities outside this set (`openLinks`, `downloadFile`, `sandbox`,\n`experimental`) are decided locally by whichever AHP client renders the\nView and are NOT part of this AHP-level advertisement — only the\nserver-derived subset is.\n\nAn agent host MUST only advertise a capability when it actually accepts the\ncorresponding methods/notifications on the `mcp://` channel:\n\n- {@link serverTools}: host proxies `tools/list` and `tools/call` to\n the MCP server. When `listChanged` is `true`, the host also forwards\n `notifications/tools/list_changed`.\n- {@link serverResources}: host proxies `resources/read`,\n `resources/list`, and `resources/templates/list` to the MCP server.\n When `listChanged` is `true`, the host also forwards\n `notifications/resources/list_changed`.\n- {@link logging}: host accepts `notifications/message` log entries\n from the App and forwards them via `mcpNotification` (and forwards\n `logging/setLevel` calls to the server).\n- {@link sampling}: host serves `sampling/createMessage` via\n `mcpMethodCall`. When `sampling.tools` is present, the host also\n accepts SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks\n inside `CreateMessageRequest`.", + "description": "Numeric question within a chat input request.", "properties": { - "serverTools": { - "type": "object", - "properties": { - "listChanged": { - "type": "boolean" - } - }, - "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "serverResources": { - "type": "object", - "properties": { - "listChanged": { - "type": "boolean" - } - }, - "description": "Producer proxies the MCP `resources/*` methods to the upstream server." + "title": { + "type": "string", + "description": "Short display title" }, - "logging": { - "type": "object", - "additionalProperties": {}, - "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + "message": { + "type": "string", + "description": "Prompt shown to the user" + }, + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "sampling": { - "type": "object", - "properties": { - "tools": { - "type": "object", - "additionalProperties": {} - } - }, - "description": "Producer serves `sampling/createMessage` via `mcpMethodCall`." - } - } - }, - "McpServerStartingState": { - "type": "object", - "description": "Server is registered with the host but has not yet started.", - "properties": { "kind": { - "const": "starting" + "oneOf": [ + { + "const": "number" + }, + { + "const": "integer" + } + ] + }, + "min": { + "type": "number", + "description": "Minimum value" + }, + "max": { + "type": "number", + "description": "Maximum value" + }, + "defaultValue": { + "type": "number", + "description": "Default numeric value" } }, "required": [ + "id", + "message", "kind" ] }, - "McpServerReadyState": { + "ChatInputBooleanQuestion": { "type": "object", - "description": "Server is running and serving requests.", + "description": "Boolean question within a chat input request.", "properties": { + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" + }, + "title": { + "type": "string", + "description": "Short display title" + }, + "message": { + "type": "string", + "description": "Prompt shown to the user" + }, + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" + }, "kind": { - "const": "ready" + "const": "boolean" + }, + "defaultValue": { + "type": "boolean", + "description": "Default boolean value" } }, "required": [ + "id", + "message", "kind" ] }, - "McpOAuthClient": { + "ChatInputSingleSelectQuestion": { "type": "object", - "description": "A pre-registered OAuth client that clients use instead of dynamic client\nregistration when resolving an MCP authentication challenge.", + "description": "Single-select question within a chat input request.", "properties": { - "clientId": { + "id": { "type": "string", - "description": "OAuth client identifier registered with the authorization server." + "description": "Stable question identifier used as the key in `answers`" }, - "clientSecret": { + "title": { "type": "string", - "description": "OAuth client secret for a confidential client. Absence means the client is\npublic and uses a secretless flow such as authorization code with PKCE." + "description": "Short display title" + }, + "message": { + "type": "string", + "description": "Prompt shown to the user" + }, + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" + }, + "kind": { + "const": "single-select" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputOption" + }, + "description": "Options the user may select from" + }, + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text instead of selecting an option" } }, "required": [ - "clientId" + "id", + "message", + "kind", + "options" ] }, - "McpAuthRequirement": { + "ChatInputMultiSelectQuestion": { "type": "object", - "description": "Reusable MCP authentication challenge — the RFC 9728 discovery info a\nclient needs to obtain a token and push it via the `authenticate` command.\nDeliberately carries **no token**: this describes what is being asked for,\nnever the bearer token itself.\n\nShared by two independent state machines that describe the same OAuth\nchallenge from different vantage points:\n\n- {@link McpServerAuthRequiredState} — the MCP server itself cannot serve\n *any* request until the client authenticates.\n- {@link ToolCallAuthRequiredState} — a specific in-flight tool call is\n paused pending authentication (typically\n {@link McpAuthRequiredReason.InsufficientScope} step-up auth\n mid-execution). The server state and the tool-call state remain\n separate on purpose: the server saying \"I need auth\" and a tool\n invocation saying \"I am waiting on that auth\" are different facts that\n can be true independently.", + "description": "Multi-select question within a chat input request.", "properties": { - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "id": { + "type": "string", + "description": "Stable question identifier used as the key in `answers`" }, - "oauthClient": { - "$ref": "#/$defs/McpOAuthClient", - "description": "Pre-registered OAuth client to use for authorization. When present, clients\nMUST use these credentials instead of dynamic client registration." + "title": { + "type": "string", + "description": "Short display title" }, - "resource": { - "$ref": "#/$defs/ProtectedResourceMetadata", - "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "requiredScopes": { + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" + }, + "kind": { + "const": "multi-select" + }, + "options": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChatInputOption" }, - "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + "description": "Options the user may select from" }, - "description": { - "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text in addition to selecting options" + }, + "min": { + "type": "number", + "description": "Minimum selected item count" + }, + "max": { + "type": "number", + "description": "Maximum selected item count" } }, "required": [ - "reason", - "resource" + "id", + "message", + "kind", + "options" ] }, - "McpServerAuthRequiredState": { + "ChatInputRequest": { "type": "object", - "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "description": "The request payload carried by an {@link InputRequestResponsePart}.\n\nThe server creates or replaces the containing response part with\n`chat/inputRequested`. Clients sync drafts with `chat/inputAnswerChanged`\nand submit responses with `chat/inputCompleted`.", "properties": { - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "id": { + "type": "string", + "description": "Stable request identifier" }, - "oauthClient": { - "$ref": "#/$defs/McpOAuthClient", - "description": "Pre-registered OAuth client to use for authorization. When present, clients\nMUST use these credentials instead of dynamic client registration." + "message": { + "type": "string", + "description": "Display message for the request as a whole" }, - "resource": { - "$ref": "#/$defs/ProtectedResourceMetadata", - "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + "url": { + "$ref": "#/$defs/URI", + "description": "URL the user should review or open, for URL-style elicitations" }, - "requiredScopes": { + "questions": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChatInputQuestion" }, - "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." - }, - "description": { - "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." + "description": "Ordered questions to ask the user" }, - "kind": { - "const": "authRequired" + "answers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ChatInputAnswer" + }, + "description": "Current draft or submitted answers, keyed by question ID" } }, "required": [ - "reason", - "resource", - "kind" + "id" ] }, - "McpServerErrorState": { + "ChatInputTextAnswerValue": { "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", + "description": "Value captured for one answer.", "properties": { "kind": { - "const": "error" + "const": "text" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details." + "value": { + "type": "string" } }, "required": [ "kind", - "error" + "value" ] }, - "McpServerStoppedState": { + "ChatInputNumberAnswerValue": { "type": "object", - "description": "Server has been shut down. The host MAY remove the server from the\nsession entirely shortly after this state.", "properties": { "kind": { - "const": "stopped" + "const": "number" + }, + "value": { + "type": "number" } }, "required": [ - "kind" + "kind", + "value" ] }, - "ChatState": { + "ChatInputBooleanAnswerValue": { "type": "object", - "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", "properties": { - "resource": { - "$ref": "#/$defs/URI", - "description": "Chat URI" - }, - "title": { - "type": "string", - "description": "Chat title" - }, - "status": { - "$ref": "#/$defs/SessionStatus", - "description": "Current chat status (reuses SessionStatus shape)" - }, - "activity": { - "type": "string", - "description": "Human-readable description of what the chat is currently doing" - }, - "modifiedAt": { - "type": "string", - "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + "kind": { + "const": "boolean" }, - "origin": { - "$ref": "#/$defs/ChatOrigin", - "description": "How this chat came into existence" + "value": { + "type": "boolean" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputSelectedAnswerValue": { + "type": "object", + "properties": { + "kind": { + "const": "selected" }, - "interactivity": { - "$ref": "#/$defs/ChatInteractivity", - "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." + "value": { + "type": "string" }, - "workingDirectories": { + "freeformValues": { "type": "array", "items": { - "$ref": "#/$defs/URI" + "type": "string" }, - "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat." + "description": "Free-form text entered instead of selecting an option" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputSelectedManyAnswerValue": { + "type": "object", + "properties": { + "kind": { + "const": "selected-many" }, - "turns": { + "value": { "type": "array", "items": { - "$ref": "#/$defs/Turn" - }, - "description": "Completed turns" - }, - "turnsNextCursor": { - "type": "string", - "description": "Cursor for loading older completed turns into this chat state.\n\nPresence means `turns` is a tail window and more historical turns are\navailable. Pass this opaque cursor to `fetchTurns`; the host MUST insert\nthe loaded turns into state and update or clear this cursor before\nresponding. Absence means the state contains all retained turns." + "type": "string" + } }, - "activeTurn": { - "$ref": "#/$defs/ActiveTurn", - "description": "Currently in-progress turn" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered in addition to selected options" + } + }, + "required": [ + "kind", + "value" + ] + }, + "ChatInputAnswered": { + "type": "object", + "properties": { + "state": { + "oneOf": [ + { + "const": "draft" + }, + { + "const": "submitted" + } + ], + "description": "Answer state" }, - "steeringMessage": { - "$ref": "#/$defs/PendingMessage", - "description": "Message to inject into the current turn at a convenient point" + "value": { + "$ref": "#/$defs/ChatInputAnswerValue", + "description": "Answer value" + } + }, + "required": [ + "state", + "value" + ] + }, + "ChatInputSkipped": { + "type": "object", + "properties": { + "state": { + "const": "skipped", + "description": "Answer state" }, - "queuedMessages": { + "freeformValues": { "type": "array", "items": { - "$ref": "#/$defs/PendingMessage" + "type": "string" }, - "description": "Messages to send automatically as new turns after the current turn finishes" - }, - "draft": { - "$ref": "#/$defs/Message", - "description": "The user's in-progress draft input for this chat — the message they are\ncomposing but have not sent yet, including its\n{@link Message.model | model} / {@link Message.agent | agent} selection\nand attachments.\n\nClients MAY periodically sync their local input state into this field so\na draft survives reloads and is visible to other clients viewing the same\nchat. Eager syncing is **not** required — clients SHOULD debounce and MAY\nsync only at convenient points. When presenting input UI for an existing\nchat, clients SHOULD use any `draft` to initialize their input state.\nCleared (set to `undefined`) once the message is sent." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this chat." + "description": "Free-form reason or value captured while skipping, if any" } }, "required": [ - "resource", - "title", - "status", - "modifiedAt", - "turns" + "state" ] }, - "ChatSummary": { + "Turn": { "type": "object", - "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", + "description": "A completed request/response cycle.", "properties": { - "resource": { - "$ref": "#/$defs/URI", - "description": "Chat URI" - }, - "title": { - "type": "string", - "description": "Chat title" - }, - "status": { - "$ref": "#/$defs/SessionStatus", - "description": "Current chat status (reuses SessionStatus shape)" - }, - "activity": { + "id": { "type": "string", - "description": "Human-readable description of what the chat is currently doing" + "description": "Turn identifier" }, - "modifiedAt": { + "startedAt": { "type": "string", - "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + "description": "ISO 8601 timestamp when this turn started." }, - "origin": { - "$ref": "#/$defs/ChatOrigin", - "description": "How this chat came into existence" + "duration": { + "type": "number", + "description": "Turn duration in milliseconds." }, - "interactivity": { - "$ref": "#/$defs/ChatInteractivity", - "description": "How the user can interact with this chat. See {@link ChatInteractivity}.\n\nSupports agent-team patterns where worker chats are read-only or hidden.\nAbsence defaults to {@link ChatInteractivity.Full} for backward\ncompatibility." + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "workingDirectories": { + "responseParts": { "type": "array", "items": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/ResponsePart" }, - "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" + }, + "state": { + "$ref": "#/$defs/TurnState", + "description": "How the turn ended" + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details if state is `'error'`" } }, "required": [ - "resource", - "title", - "status", - "modifiedAt" + "id", + "message", + "responseParts", + "state" ] }, - "SideChatSelection": { + "ActiveTurn": { "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.", + "description": "An in-progress turn — the assistant is actively streaming.", "properties": { - "text": { + "id": { "type": "string", - "description": "Exact selected-text snapshot captured at `createChat` acceptance.\n\nMUST be non-empty." + "description": "Turn identifier" }, - "responsePartId": { + "startedAt": { "type": "string", - "description": "Optional provenance for the response part that contained {@link text} when\nthe host took the snapshot.\n\nAdvisory only: this is not a live range or offset and MUST NOT be used to\nrecompute `text`." + "description": "ISO 8601 timestamp when this turn started." + }, + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" + }, + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" } }, "required": [ - "text" + "id", + "startedAt", + "message", + "responseParts" ] }, - "PendingMessage": { + "MessageOrigin": { "type": "object", - "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", + "description": "Identifies the origin of a {@link Message} — who produced it. For the message\nthat initiates a turn ({@link Turn.message}), this is also the origin of the\nturn; for steering or queued messages it is just the origin of that message.", "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this pending message" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that will start the next turn" + "kind": { + "$ref": "#/$defs/MessageKind", + "description": "The kind of actor that produced the message." } }, "required": [ - "id", - "message" + "kind" ] }, - "ChatInputOption": { + "Message": { "type": "object", - "description": "A choice in a select-style question.", + "description": "A message that initiates or steers a turn. Messages can originate from the\nuser, the agent, a tool, or be system-generated (see {@link MessageOrigin}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", "properties": { - "id": { + "text": { "type": "string", - "description": "Stable option identifier; for MCP enum values this is the enum string" + "description": "Message text" }, - "label": { - "type": "string", - "description": "Display label" + "origin": { + "$ref": "#/$defs/MessageOrigin", + "description": "The origin of the message" }, - "description": { - "type": "string", - "description": "Optional secondary text" + "attachments": { + "type": "array", + "items": { + "$ref": "#/$defs/MessageAttachment" + }, + "description": "File/selection attachments" }, - "recommended": { - "type": "boolean", - "description": "Whether this option is the recommended/default choice" + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "The model this message was, or will be, sent with.\n\nFor historic user/agent messages this records the model actually used, so\na client editing or resending the message can retain that selection. For a\n{@link ChatState.draft | draft} it carries the model the user picked for\nthe message they are composing. Absent means the agent host's default\nmodel applies." + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "The custom agent this message was, or will be, sent with.\n\nFor historic messages this records the agent actually used; for a\n{@link ChatState.draft | draft} it carries the agent the user picked.\nAbsent means no custom agent — the provider's default behavior applies." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." } }, "required": [ - "id", - "label" + "text", + "origin" ] }, - "ChatInputQuestionBase": { + "MessageAttachmentBase": { "type": "object", + "description": "Common fields shared by all {@link MessageAttachment} variants.", "properties": { - "id": { + "label": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "title": { - "type": "string", - "description": "Short display title" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "message": { + "displayKind": { "type": "string", - "description": "Prompt shown to the user" + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." } }, "required": [ - "id", - "message" + "label" ] }, - "ChatInputTextQuestion": { + "SimpleMessageAttachment": { "type": "object", - "description": "Text question within a chat input request.", + "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { + "label": { "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "kind": { - "const": "text" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "format": { + "displayKind": { "type": "string", - "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "min": { - "type": "number", - "description": "Minimum string length" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "max": { - "type": "number", - "description": "Maximum string length" + "type": { + "const": "simple", + "description": "Discriminant" }, - "defaultValue": { + "modelRepresentation": { "type": "string", - "description": "Default text" + "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." } }, "required": [ - "id", - "message", - "kind" + "label", + "type" ] }, - "ChatInputNumberQuestion": { + "MessageEmbeddedResourceAttachment": { "type": "object", - "description": "Numeric question within a chat input request.", + "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", "properties": { - "id": { + "label": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "title": { - "type": "string", - "description": "Short display title" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "message": { + "displayKind": { "type": "string", - "description": "Prompt shown to the user" + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "kind": { - "oneOf": [ - { - "const": "number" - }, - { - "const": "integer" - } - ] + "type": { + "const": "embeddedResource", + "description": "Discriminant" }, - "min": { - "type": "number", - "description": "Minimum value" + "data": { + "type": "string", + "description": "Base64-encoded binary data" }, - "max": { - "type": "number", - "description": "Maximum value" + "contentType": { + "type": "string", + "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" }, - "defaultValue": { - "type": "number", - "description": "Default numeric value" + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", - "message", - "kind" + "label", + "type", + "data", + "contentType" ] }, - "ChatInputBooleanQuestion": { + "MessageResourceAttachment": { "type": "object", - "description": "Boolean question within a chat input request.", + "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", "properties": { - "id": { + "label": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "title": { + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { "type": "string", - "description": "Short display title" + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "message": { + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { "type": "string", - "description": "Prompt shown to the user" + "description": "Content MIME type" }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "nonce": { + "type": "string", + "description": "Content nonce" }, - "kind": { - "const": "boolean" + "type": { + "const": "resource", + "description": "Discriminant" }, - "defaultValue": { - "type": "boolean", - "description": "Default boolean value" + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." } }, "required": [ - "id", - "message", - "kind" + "label", + "uri", + "type" ] }, - "ChatInputSingleSelectQuestion": { + "MessageAnnotationsAttachment": { "type": "object", - "description": "Single-select question within a chat input request.", + "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", "properties": { - "id": { + "label": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "title": { - "type": "string", - "description": "Short display title" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "message": { + "displayKind": { "type": "string", - "description": "Prompt shown to the user" + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "kind": { - "const": "single-select" + "type": { + "const": "annotations", + "description": "Discriminant" }, - "options": { + "resource": { + "$ref": "#/$defs/URI", + "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + }, + "annotationIds": { "type": "array", "items": { - "$ref": "#/$defs/ChatInputOption" + "type": "string" }, - "description": "Options the user may select from" - }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text instead of selecting an option" + "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." } }, "required": [ - "id", - "message", - "kind", - "options" + "label", + "type", + "resource" ] }, - "ChatInputMultiSelectQuestion": { + "MessageChatAttachment": { "type": "object", - "description": "Multi-select question within a chat input request.", + "description": "An attachment that references a chat transcript through a fixed completed\nturn.\n\nThe referenced chat MAY belong to a different session than the message's\nchat. The attachment's model representation identifies the chat in a way\nthat hosts can resolve regardless of the session that owns it.\n\nWhen `endTurn` is omitted, the host MUST resolve and pin the referenced\nchat's latest completed turn when accepting the message. This lets clients\nattach a chat without knowing its turn identifiers. When provided, `endTurn`\nMUST reference a completed, retained turn. The host resolves the transcript\nfrom its first retained turn through the pinned turn, inclusive. Later turns\ndo not change the context represented by an already-sent attachment.\n\nWhen the referenced chat has no completed retained turns, the resolved\ntranscript is empty and hosts MUST NOT reject the attachment on that basis.\n\nHosts MUST NOT recursively expand chat attachments found inside the\nreferenced transcript. Clients SHOULD keep rendering `label` if the\nreferenced chat is later pruned, and treat opening `resource` as best-effort.", "properties": { - "id": { + "label": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." }, - "title": { - "type": "string", - "description": "Short display title" + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." }, - "message": { + "displayKind": { "type": "string", - "description": "Prompt shown to the user" + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." }, - "kind": { - "const": "multi-select" + "type": { + "const": "chat", + "description": "Discriminant" }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ChatInputOption" - }, - "description": "Options the user may select from" + "resource": { + "$ref": "#/$defs/URI", + "description": "URI of the referenced chat." }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text in addition to selecting options" + "endTurn": { + "type": "string", + "description": "Last completed turn included in the referenced transcript. When omitted,\nthe host pins the latest completed turn when accepting the message." + } + }, + "required": [ + "label", + "type", + "resource" + ] + }, + "MarkdownResponsePart": { + "type": "object", + "properties": { + "kind": { + "const": "markdown", + "description": "Discriminant" }, - "min": { - "type": "number", - "description": "Minimum selected item count" + "id": { + "type": "string", + "description": "Part identifier, used by `chat/delta` to target this part for content appends" }, - "max": { - "type": "number", - "description": "Maximum selected item count" + "content": { + "type": "string", + "description": "Markdown content" } }, "required": [ - "id", - "message", "kind", - "options" + "id", + "content" ] }, - "ChatInputRequest": { + "ResourceResponsePart": { "type": "object", - "description": "The request payload carried by an {@link InputRequestResponsePart}.\n\nThe server creates or replaces the containing response part with\n`chat/inputRequested`. Clients sync drafts with `chat/inputAnswerChanged`\nand submit responses with `chat/inputCompleted`.", + "description": "A content part that's a reference to large content stored outside the state tree.", "properties": { - "id": { - "type": "string", - "description": "Stable request identifier" + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" }, - "message": { - "type": "string", - "description": "Display message for the request as a whole" + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "url": { - "$ref": "#/$defs/URI", - "description": "URL the user should review or open, for URL-style elicitations" + "contentType": { + "type": "string", + "description": "Content MIME type" }, - "questions": { - "type": "array", - "items": { - "$ref": "#/$defs/ChatInputQuestion" - }, - "description": "Ordered questions to ask the user" + "nonce": { + "type": "string", + "description": "Content nonce" }, - "answers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ChatInputAnswer" - }, - "description": "Current draft or submitted answers, keyed by question ID" + "kind": { + "const": "contentRef", + "description": "Discriminant" } }, "required": [ - "id" + "uri", + "kind" ] }, - "ChatInputTextAnswerValue": { + "ToolCallResponsePart": { "type": "object", - "description": "Value captured for one answer.", + "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", "properties": { "kind": { - "const": "text" + "const": "toolCall", + "description": "Discriminant" }, - "value": { - "type": "string" + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "Full tool call lifecycle state" } }, "required": [ "kind", - "value" + "toolCall" ] }, - "ChatInputNumberAnswerValue": { + "ReasoningResponsePart": { "type": "object", + "description": "Reasoning/thinking content from the model.", "properties": { "kind": { - "const": "number" + "const": "reasoning", + "description": "Discriminant" }, - "value": { - "type": "number" + "id": { + "type": "string", + "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" + }, + "content": { + "type": "string", + "description": "Accumulated reasoning text" } }, "required": [ "kind", - "value" + "id", + "content" ] }, - "ChatInputBooleanAnswerValue": { + "InputRequestResponsePart": { "type": "object", + "description": "A live or resolved input request (elicitation) in the turn response stream.\n\nThe server inserts the part with `chat/inputRequested`. While\n{@link response} is absent, clients can update answer drafts with\n`chat/inputAnswerChanged` and submit a response with `chat/inputCompleted`.\nCompletion updates this part in place so its stream position is stable and\nthe full interaction remains durable and backfillable via `fetchTurns`.\n\nIf the turn ends without a submitted response, the unresolved part remains\nin the completed turn transcript with {@link response} absent.", "properties": { "kind": { - "const": "boolean" + "const": "inputRequest", + "description": "Discriminant" }, - "value": { - "type": "boolean" + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The request, carrying its `id`, `message`, `url`, `questions`, and current\ndraft or submitted `answers`." + }, + "response": { + "$ref": "#/$defs/ChatInputResponseKind", + "description": "How the request was resolved. Absent until a client submits `accept`,\n`decline`, or `cancel` with `chat/inputCompleted`." } }, "required": [ "kind", - "value" + "request" ] }, - "ChatInputSelectedAnswerValue": { + "SystemNotificationResponsePart": { "type": "object", + "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", "properties": { "kind": { - "const": "selected" + "const": "systemNotification", + "description": "Discriminant" }, - "value": { - "type": "string" + "content": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "The text of the system notification" }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form text entered instead of selecting an option" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this notification.\n\nA host MAY attach a machine-readable descriptor of what triggered the\nnotification so clients can categorize, icon, group, filter, or localize\nit without parsing `content`. Clients MAY look for well-known keys here to\nprovide enhanced UI, and MUST render coherently from `content` alone when\n`_meta` is absent or unrecognized." } }, "required": [ "kind", - "value" + "content" ] }, - "ChatInputSelectedManyAnswerValue": { + "ToolCallRiskAssessmentBase": { "type": "object", "properties": { "kind": { - "const": "selected-many" - }, - "value": { - "type": "array", - "items": { - "type": "string" - } - }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form text entered in addition to selected options" + "$ref": "#/$defs/ToolCallRiskAssessmentKind" } }, "required": [ - "kind", - "value" + "kind" ] }, - "ChatInputAnswered": { + "ToolCallRiskAssessmentLoadingState": { "type": "object", + "description": "The model judge is still evaluating the tool call.", "properties": { - "state": { - "oneOf": [ - { - "const": "draft" - }, - { - "const": "submitted" - } - ], - "description": "Answer state" + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" }, - "value": { - "$ref": "#/$defs/ChatInputAnswerValue", - "description": "Answer value" + "status": { + "const": "loading" } }, "required": [ - "state", - "value" + "kind", + "status" ] }, - "ChatInputSkipped": { + "ToolCallRiskAssessmentCompleteState": { "type": "object", + "description": "The model judge has completed its evaluation.", "properties": { - "state": { - "const": "skipped", - "description": "Answer state" + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form reason or value captured while skipping, if any" + "status": { + "const": "complete" + }, + "reason": { + "$ref": "#/$defs/StringOrMarkdown" + }, + "safety": { + "type": "number", + "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." } }, "required": [ - "state" + "kind", + "status", + "reason", + "safety" ] }, - "Turn": { + "ConfirmationOption": { "type": "object", - "description": "A completed request/response cycle.", + "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Unique identifier for the option, returned in the confirmed action" }, - "startedAt": { + "label": { "type": "string", - "description": "ISO 8601 timestamp when this turn started." - }, - "duration": { - "type": "number", - "description": "Turn duration in milliseconds." - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "description": "Human-readable label displayed to the user" }, - "responseParts": { - "type": "array", - "items": { - "$ref": "#/$defs/ResponsePart" - }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nConsumers should derive display text by concatenating markdown parts,\nand find tool calls by filtering for `ToolCall` parts." + "kind": { + "$ref": "#/$defs/ConfirmationOptionKind", + "description": "Whether this option represents an approval or denial" }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "group": { + "type": "number", + "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + } + }, + "required": [ + "id", + "label", + "kind" + ] + }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "const": "client" }, - "state": { - "$ref": "#/$defs/TurnState", - "description": "How the turn ended" + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "const": "mcp" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Error details if state is `'error'`" + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." } }, "required": [ - "id", - "message", - "responseParts", - "state" + "kind", + "customizationId" ] }, - "ActiveTurn": { + "ToolCallBase": { "type": "object", - "description": "An in-progress turn — the assistant is actively streaming.", + "description": "Metadata common to all tool call states.", "properties": { - "id": { + "toolCallId": { "type": "string", - "description": "Turn identifier" + "description": "Unique tool call identifier" }, - "startedAt": { + "toolName": { "type": "string", - "description": "ISO 8601 timestamp when this turn started." + "description": "Internal tool name (for debugging/logging)" }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "displayName": { + "type": "string", + "description": "Human-readable tool name" }, - "responseParts": { - "type": "array", - "items": { - "$ref": "#/$defs/ResponsePart" - }, - "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." } }, "required": [ - "id", - "startedAt", - "message", - "responseParts" + "toolCallId", + "toolName", + "displayName" ] }, - "MessageOrigin": { + "ToolCallParameterFields": { "type": "object", - "description": "Identifies the origin of a {@link Message} — who produced it. For the message\nthat initiates a turn ({@link Turn.message}), this is also the origin of the\nturn; for steering or queued messages it is just the origin of that message.", + "description": "Properties available once tool call parameters are fully received.", "properties": { - "kind": { - "$ref": "#/$defs/MessageKind", - "description": "The kind of actor that produced the message." + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." } }, "required": [ - "kind" + "invocationMessage" ] }, - "Message": { + "ToolCallResult": { "type": "object", - "description": "A message that initiates or steers a turn. Messages can originate from the\nuser, the agent, a tool, or be system-generated (see {@link MessageOrigin}).\n\nAttachments MAY be referenced inside {@link Message.text} via their\n{@link MessageAttachmentBase.range} field. Attachments without a range are\nstill associated with the message but do not correspond to a specific span\nin the text.", + "description": "Tool execution result details, available after execution completes.", "properties": { - "text": { - "type": "string", - "description": "Message text" + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" }, - "origin": { - "$ref": "#/$defs/MessageOrigin", - "description": "The origin of the message" + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" }, - "attachments": { + "content": { "type": "array", "items": { - "$ref": "#/$defs/MessageAttachment" + "$ref": "#/$defs/ToolResultContent" }, - "description": "File/selection attachments" - }, - "model": { - "$ref": "#/$defs/ModelSelection", - "description": "The model this message was, or will be, sent with.\n\nFor historic user/agent messages this records the model actually used, so\na client editing or resending the message can retain that selection. For a\n{@link ChatState.draft | draft} it carries the model the user picked for\nthe message they are composing. Absent means the agent host's default\nmodel applies." - }, - "agent": { - "$ref": "#/$defs/AgentSelection", - "description": "The custom agent this message was, or will be, sent with.\n\nFor historic messages this records the agent actually used; for a\n{@link ChatState.draft | draft} it carries the agent the user picked.\nAbsent means no custom agent — the provider's default behavior applies." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "_meta": { + "structuredContent": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" } }, "required": [ - "text", - "origin" + "success", + "pastTenseMessage" ] }, - "MessageAttachmentBase": { + "ToolCallStreamingState": { "type": "object", - "description": "Common fields shared by all {@link MessageAttachment} variants.", + "description": "LM is streaming the tool call parameters.", "properties": { - "label": { + "toolCallId": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Unique tool call identifier" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "displayKind": { + "displayName": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable tool name" + }, + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "status": { + "const": "streaming" + }, + "partialInput": { + "type": "string", + "description": "Partial parameters accumulated from tool-call deltas." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Progress message shown while parameters are streaming" } }, "required": [ - "label" + "toolCallId", + "toolName", + "displayName", + "status" ] }, - "SimpleMessageAttachment": { + "ToolCallPendingConfirmationState": { "type": "object", - "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", + "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", "properties": { - "label": { + "toolCallId": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Unique tool call identifier" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "displayKind": { + "displayName": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable tool name" + }, + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "const": "simple", - "description": "Discriminant" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + }, + "status": { + "const": "pending-confirmation" + }, + "confirmationTitle": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, + "edits": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/FileEdit" + } + } + }, + "required": [ + "items" + ], + "description": "File edits that this tool call will perform, for preview before confirmation" + }, + "editable": { + "type": "boolean", + "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ConfirmationOption" + }, + "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status" + ] + }, + "ToolCallPostConfirmationFields": { + "type": "object", + "description": "Fields present on every tool call state that exists **after** confirmation\nhas been resolved: {@link ToolCallRunningState}, {@link ToolCallAuthRequiredState},\n{@link ToolCallPendingResultConfirmationState}, and {@link ToolCallCompletedState}.\n`ToolCallPendingConfirmationState` (not yet confirmed) and\n`ToolCallCancelledState` (the denial path — never ran) don't satisfy this\ninvariant, so they keep their own `selectedOption` field independently\nrather than extending this one.", + "properties": { + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" }, - "modelRepresentation": { - "type": "string", - "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "label", - "type" + "confirmed" ] }, - "MessageEmbeddedResourceAttachment": { + "ToolCallRunningState": { "type": "object", - "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "description": "Tool is actively executing.", "properties": { - "label": { + "toolCallId": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Unique tool call identifier" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "displayKind": { + "displayName": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable tool name" + }, + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "const": "embeddedResource", - "description": "Discriminant" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "data": { - "type": "string", - "description": "Base64-encoded binary data" + "toolInput": { + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, - "contentType": { - "type": "string", - "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + }, + "status": { + "const": "running" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." } }, "required": [ - "label", - "type", - "data", - "contentType" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "confirmed", + "status" ] }, - "MessageResourceAttachment": { + "ToolCallAuthRequiredState": { "type": "object", - "description": "An attachment that references a resource by URI. The content is not\ndelivered inline; consumers can fetch it via `resourceRead` when needed.", + "description": "A running tool call is paused because the MCP server backing it needs\nauthentication — most commonly {@link McpAuthRequirement.reason |\n`insufficientScope`} step-up auth triggered by the `tools/call` request\nitself. Only ever reached from {@link ToolCallRunningState}, and normally\nreturns there once authenticated: `running` → `auth-required` → `running`\n→ …. A client MAY instead cancel the invocation without authenticating by\ndispatching a `chat/toolCallComplete` with a **failed** result, always\nmoving straight to {@link ToolCallCompletedState} —\n`requiresResultConfirmation` is ignored on this path, so it can never\nenter {@link ToolCallPendingResultConfirmationState}. A **successful**\nresult dispatched from this state is invalid and MUST be rejected/ignored\nas a no-op by the reducer, since execution never resumed after the\nchallenge.\n\nThis is the tool-call-level counterpart to\n{@link McpServerAuthRequiredState} — that state means the MCP *server*\ncannot serve any request; this one means *this specific invocation* is\nwaiting on the same kind of challenge. The two are dispatched\nindependently and MAY be true at the same time, or not: an\n`insufficientScope` challenge triggered by a single tool call, for\nexample, need not block the whole server.\n\nBecause the challenge is always resolved by pushing a token via the\nexisting `authenticate` command, this state can only originate from a\ntool call {@link ToolCallContributorKind.MCP | contributed by an MCP\nserver} — `contributor` is narrowed accordingly (unlike the optional,\nmulti-kind `contributor` on other tool call states).", "properties": { - "label": { + "toolCallId": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Unique tool call identifier" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "displayKind": { + "displayName": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable tool name" + }, + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" + }, + "contributor": { + "$ref": "#/$defs/ToolCallMcpContributor", + "description": "The MCP server that contributed this tool call — always MCP, never a client tool." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "toolInput": { + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" }, - "nonce": { - "type": "string", - "description": "Content nonce" + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" }, - "type": { - "const": "resource", - "description": "Discriminant" + "status": { + "const": "auth-required" }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + "auth": { + "$ref": "#/$defs/McpAuthRequirement", + "description": "The authentication challenge blocking this invocation." + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Partial content produced before the call paused for authentication." } }, "required": [ - "label", - "uri", - "type" + "toolCallId", + "toolName", + "displayName", + "contributor", + "invocationMessage", + "confirmed", + "status", + "auth" ] }, - "MessageAnnotationsAttachment": { + "ToolCallPendingResultConfirmationState": { "type": "object", - "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", + "description": "Tool finished executing, waiting for client to approve the result.", "properties": { - "label": { + "toolCallId": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Unique tool call identifier" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "displayKind": { + "displayName": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable tool name" + }, + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "const": "annotations", - "description": "Discriminant" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "resource": { - "$ref": "#/$defs/URI", - "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + "toolInput": { + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, - "annotationIds": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ToolResultContent" }, - "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + }, + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + }, + "status": { + "const": "pending-result-confirmation" } }, "required": [ - "label", - "type", - "resource" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "confirmed", + "status" ] }, - "MessageChatAttachment": { + "ToolCallCompletedState": { "type": "object", - "description": "An attachment that references a chat transcript through a fixed completed\nturn.\n\nThe referenced chat MAY belong to a different session than the message's\nchat. The attachment's model representation identifies the chat in a way\nthat hosts can resolve regardless of the session that owns it.\n\nWhen `endTurn` is omitted, the host MUST resolve and pin the referenced\nchat's latest completed turn when accepting the message. This lets clients\nattach a chat without knowing its turn identifiers. When provided, `endTurn`\nMUST reference a completed, retained turn. The host resolves the transcript\nfrom its first retained turn through the pinned turn, inclusive. Later turns\ndo not change the context represented by an already-sent attachment.\n\nWhen the referenced chat has no completed retained turns, the resolved\ntranscript is empty and hosts MUST NOT reject the attachment on that basis.\n\nHosts MUST NOT recursively expand chat attachments found inside the\nreferenced transcript. Clients SHOULD keep rendering `label` if the\nreferenced chat is later pruned, and treat opening `resource` as best-effort.", + "description": "Tool completed successfully or with an error.", "properties": { - "label": { + "toolCallId": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Unique tool call identifier" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" }, - "displayKind": { + "displayName": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Human-readable tool name" + }, + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" + }, + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." }, - "type": { - "const": "chat", - "description": "Discriminant" + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" }, - "resource": { - "$ref": "#/$defs/URI", - "description": "URI of the referenced chat." + "toolInput": { + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, - "endTurn": { - "type": "string", - "description": "Last completed turn included in the referenced transcript. When omitted,\nthe host pins the latest completed turn when accepting the message." - } - }, - "required": [ - "label", - "type", - "resource" - ] - }, - "MarkdownResponsePart": { - "type": "object", - "properties": { - "kind": { - "const": "markdown", - "description": "Discriminant" + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" }, - "id": { - "type": "string", - "description": "Part identifier, used by `chat/delta` to target this part for content appends" + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" }, "content": { - "type": "string", - "description": "Markdown content" - } - }, - "required": [ - "kind", - "id", - "content" - ] - }, - "ResourceResponsePart": { - "type": "object", - "description": "A content part that's a reference to large content stored outside the state tree.", - "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "nonce": { - "type": "string", - "description": "Content nonce" + "confirmed": { + "$ref": "#/$defs/ToolCallConfirmationReason", + "description": "How the tool was confirmed for execution" }, - "kind": { - "const": "contentRef", - "description": "Discriminant" - } - }, - "required": [ - "uri", - "kind" - ] - }, - "ToolCallResponsePart": { - "type": "object", - "description": "A tool call represented as a response part.\n\nTool calls are part of the response stream, interleaved with text and\nreasoning. The `toolCall.toolCallId` serves as the part identifier for\nactions that target this part.", - "properties": { - "kind": { - "const": "toolCall", - "description": "Discriminant" + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "Full tool call lifecycle state" + "status": { + "const": "completed" } }, "required": [ - "kind", - "toolCall" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "confirmed", + "status" ] }, - "ReasoningResponsePart": { + "ToolCallCancelledState": { "type": "object", - "description": "Reasoning/thinking content from the model.", + "description": "Tool call was cancelled before execution.", "properties": { - "kind": { - "const": "reasoning", - "description": "Discriminant" - }, - "id": { + "toolCallId": { "type": "string", - "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" + "description": "Unique tool call identifier" }, - "content": { + "toolName": { "type": "string", - "description": "Accumulated reasoning text" - } - }, - "required": [ - "kind", - "id", - "content" - ] - }, - "InputRequestResponsePart": { - "type": "object", - "description": "A live or resolved input request (elicitation) in the turn response stream.\n\nThe server inserts the part with `chat/inputRequested`. While\n{@link response} is absent, clients can update answer drafts with\n`chat/inputAnswerChanged` and submit a response with `chat/inputCompleted`.\nCompletion updates this part in place so its stream position is stable and\nthe full interaction remains durable and backfillable via `fetchTurns`.\n\nIf the turn ends without a submitted response, the unresolved part remains\nin the completed turn transcript with {@link response} absent.", - "properties": { - "kind": { - "const": "inputRequest", - "description": "Discriminant" + "description": "Internal tool name (for debugging/logging)" }, - "request": { - "$ref": "#/$defs/ChatInputRequest", - "description": "The request, carrying its `id`, `message`, `url`, `questions`, and current\ndraft or submitted `answers`." + "displayName": { + "type": "string", + "description": "Human-readable tool name" }, - "response": { - "$ref": "#/$defs/ChatInputResponseKind", - "description": "How the request was resolved. Absent until a client submits `accept`,\n`decline`, or `cancel` with `chat/inputCompleted`." - } - }, - "required": [ - "kind", - "request" - ] - }, - "SystemNotificationResponsePart": { - "type": "object", - "description": "A system notification surfaced as part of the response stream.\n\nSystem notifications are messages authored by the agent harness\nthat need to be visible to both the agent (for situational awareness) and\nthe user (for transcript continuity). Examples include \"background subagent\nX completed\" or \"task Y was cancelled\".", - "properties": { - "kind": { - "const": "systemNotification", - "description": "Discriminant" + "intention": { + "type": "string", + "description": "Human-readable description of what the tool invocation intends to do" }, - "content": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "The text of the system notification" + "contributor": { + "$ref": "#/$defs/ToolCallContributor", + "description": "Reference to the contributor of the tool being called." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this notification.\n\nA host MAY attach a machine-readable descriptor of what triggered the\nnotification so clients can categorize, icon, group, filter, or localize\nit without parsing `content`. Clients MAY look for well-known keys here to\nprovide enhanced UI, and MUST render coherently from `content` alone when\n`_meta` is absent or unrecognized." + "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + }, + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + }, + "status": { + "const": "cancelled" + }, + "reason": { + "$ref": "#/$defs/ToolCallCancellationReason", + "description": "Why the tool was cancelled" + }, + "reasonMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional message explaining the cancellation" + }, + "userSuggestion": { + "$ref": "#/$defs/Message", + "description": "What the user suggested doing instead" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" } }, "required": [ - "kind", - "content" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "reason" ] }, - "ToolCallRiskAssessmentBase": { + "ToolResultTextContent": { "type": "object", + "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallRiskAssessmentKind" + "type": { + "const": "text" + }, + "text": { + "type": "string", + "description": "The text content" } }, "required": [ - "kind" + "type", + "text" ] }, - "ToolCallRiskAssessmentLoadingState": { + "ToolResultEmbeddedResourceContent": { "type": "object", - "description": "The model judge is still evaluating the tool call.", + "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallRiskAssessmentKind" + "type": { + "const": "embeddedResource" }, - "status": { - "const": "loading" + "data": { + "type": "string", + "description": "Base64-encoded data" + }, + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" } }, "required": [ - "kind", - "status" + "type", + "data", + "contentType" ] }, - "ToolCallRiskAssessmentCompleteState": { + "ToolResultResourceContent": { "type": "object", - "description": "The model judge has completed its evaluation.", + "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallRiskAssessmentKind" + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" }, - "status": { - "const": "complete" + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "reason": { - "$ref": "#/$defs/StringOrMarkdown" + "contentType": { + "type": "string", + "description": "Content MIME type" }, - "safety": { - "type": "number", - "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "type": { + "const": "resource" } }, "required": [ - "kind", - "status", - "reason", - "safety" + "uri", + "type" ] }, - "ConfirmationOption": { + "ToolResultFileEditContent": { "type": "object", - "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "description": "Describes a file modification performed by a tool.", "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the option, returned in the confirmed action" + "before": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI" + }, + "content": { + "$ref": "#/$defs/ContentRef" + } + }, + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." }, - "label": { - "type": "string", - "description": "Human-readable label displayed to the user" + "after": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI" + }, + "content": { + "$ref": "#/$defs/ContentRef" + } + }, + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." }, - "kind": { - "$ref": "#/$defs/ConfirmationOptionKind", - "description": "Whether this option represents an approval or denial" + "diff": { + "type": "object", + "properties": { + "added": { + "type": "number" + }, + "removed": { + "type": "number" + } + }, + "description": "Optional diff display metadata" }, - "group": { - "type": "number", - "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + "type": { + "const": "fileEdit" } }, "required": [ - "id", - "label", - "kind" + "type" ] }, - "ToolCallClientContributor": { + "ToolResultTerminalContent": { "type": "object", + "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.\n\nWhen the command exits, {@link result} is filled in on the completed\nresult, retaining the outcome for clients that did not subscribe. This\nrecords the command's exit, not the terminal's — the terminal may keep\nrunning afterwards.", "properties": { - "kind": { - "const": "client" + "type": { + "const": "terminal" }, - "clientId": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" + }, + "title": { "type": "string", - "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." + "description": "Display title for the terminal content" + }, + "isPty": { + "type": "boolean", + "description": "Whether this terminal-style resource is backed by a pseudoterminal.\nWhen `false`, output is plain text and clients do not need to parse\nVT sequences." + }, + "result": { + "$ref": "#/$defs/TerminalCommandResult", + "description": "Outcome of the command, present once it has exited." } }, "required": [ - "kind", - "clientId" + "type", + "resource", + "title" ] }, - "ToolCallMcpContributor": { + "TerminalCommandResult": { "type": "object", + "description": "Outcome of a command run in a terminal-style tool, filled in on\n{@link ToolResultTerminalContent.result} once the command exits.", "properties": { - "kind": { - "const": "mcp" + "exitCode": { + "type": "number", + "description": "Exit code from the completed command, if reported by the runtime" }, - "customizationId": { + "preview": { "type": "string", - "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + "description": "Preview of the command's output, for clients that are not subscribed\nto the terminal or that arrive after it is disposed. When `isPty` is\n`true` the preview may contain VT sequences; when `false` it is plain\ntext." + }, + "truncated": { + "type": "boolean", + "description": "Whether `preview` is known to be incomplete or truncated" } - }, - "required": [ - "kind", - "customizationId" - ] + } }, - "ToolCallBase": { + "ToolResultSubagentContent": { "type": "object", - "description": "Metadata common to all tool call states.", + "description": "A reference, embedded in a tool result, to a worker chat spawned by the tool\ncall (a sub-agent delegation), referenced by a chat URI (`ahp-chat:/...`).\n\nThis is the spawning tool call's forward view of the worker. The worker chat\nrecords the same edge in reverse via its {@link ChatOrigin} (`kind: 'tool'`),\nwhose `toolCallId` identifies the tool call that emitted this content.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" + "type": { + "const": "subagent" }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "resource": { + "$ref": "#/$defs/URI", + "description": "Worker chat URI (subscribable for full chat state)" }, - "displayName": { + "title": { "type": "string", - "description": "Human-readable tool name" + "description": "Display title for the subagent" }, - "intention": { + "agentName": { "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "description": "Internal agent name" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "description": { + "type": "string", + "description": "Human-readable description of the subagent's task" } }, "required": [ - "toolCallId", - "toolName", - "displayName" + "type", + "resource", + "title" ] }, - "ToolCallParameterFields": { + "TerminalInfo": { "type": "object", - "description": "Properties available once tool call parameters are fully received.", + "description": "Lightweight terminal metadata exposed on the root state.", "properties": { - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "resource": { + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" }, - "toolInput": { - "$ref": "#/$defs/ToolInput", - "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + "title": { + "type": "string", + "description": "Human-readable terminal title" + }, + "claim": { + "$ref": "#/$defs/TerminalClaim", + "description": "Who currently holds this terminal" + }, + "exitCode": { + "type": "number", + "description": "Process exit code, if the terminal process has exited" } }, "required": [ - "invocationMessage" + "resource", + "title", + "claim" ] }, - "ToolCallResult": { + "TerminalClientClaim": { "type": "object", - "description": "Tool execution result details, available after execution completes.", + "description": "A terminal claimed by a connected client.", "properties": { - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" - }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "kind": { + "const": "client", + "description": "Discriminant" }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "clientId": { + "type": "string", + "description": "The `clientId` of the claiming client" } }, "required": [ - "success", - "pastTenseMessage" + "kind", + "clientId" ] }, - "ToolCallStreamingState": { + "TerminalSessionClaim": { "type": "object", - "description": "LM is streaming the tool call parameters.", + "description": "A terminal claimed by a session, optionally scoped to a specific turn or tool call.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "kind": { + "const": "session", + "description": "Discriminant" }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" + "session": { + "$ref": "#/$defs/URI", + "description": "Session URI that claimed the terminal" }, - "intention": { + "turnId": { "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "status": { - "const": "streaming" + "description": "Optional turn identifier within the session" }, - "partialInput": { + "toolCallId": { "type": "string", - "description": "Partial parameters accumulated from tool-call deltas." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Progress message shown while parameters are streaming" + "description": "Optional tool call identifier within the turn" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "status" + "kind", + "session" ] }, - "ToolCallPendingConfirmationState": { + "TerminalState": { "type": "object", - "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "description": "Full state for a single terminal, loaded when a client subscribes to the terminal's URI.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" - }, - "intention": { + "title": { "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "description": "Human-readable terminal title" }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "cwd": { + "$ref": "#/$defs/URI", + "description": "Current working directory of the terminal process" }, - "toolInput": { - "$ref": "#/$defs/ToolInput", - "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + "cols": { + "type": "number", + "description": "Terminal width in columns" }, - "status": { - "const": "pending-confirmation" + "rows": { + "type": "number", + "description": "Terminal height in rows" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/TerminalContentPart" + }, + "description": "Typed content parts, replacing the flat `content: string`.\n\nNaive consumers that only need the raw VT stream can reconstruct it with:\n `content.map(p => p.type === 'command' ? p.output : p.value).join('')`\n\nConsumers that need command boundaries can filter by part type." }, - "riskAssessment": { - "$ref": "#/$defs/ToolCallRiskAssessment", - "description": "Risk assessment that informed the confirmation requirement." + "exitCode": { + "type": "number", + "description": "Process exit code, set when the terminal process exits" }, - "edits": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/$defs/FileEdit" - } - } - }, - "required": [ - "items" - ], - "description": "File edits that this tool call will perform, for preview before confirmation" + "claim": { + "$ref": "#/$defs/TerminalClaim", + "description": "Who currently holds this terminal" }, - "editable": { + "supportsCommandDetection": { "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + "description": "Whether this terminal emits `terminal/commandExecuted` and\n`terminal/commandFinished` actions and populates `command`-typed parts.\n\nClients MUST check this flag before relying on command detection.\nDo NOT use the presence of a `command` part as a feature flag — parts\nare absent in the normal idle state." }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ConfirmationOption" - }, - "description": "Options the server offers for this confirmation. When present, the client\nSHOULD render these instead of a plain approve/deny UI. Each option\nbelongs to a {@link ConfirmationOptionGroup} so the client can still\ncategorise the choices." + "isPty": { + "type": "boolean", + "description": "Whether this terminal-style resource is backed by a pseudoterminal.\nWhen `false`, output is plain text and clients do not need to parse\nVT sequences." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status" + "title", + "content", + "claim" ] }, - "ToolCallPostConfirmationFields": { + "TerminalUnclassifiedPart": { "type": "object", - "description": "Fields present on every tool call state that exists **after** confirmation\nhas been resolved: {@link ToolCallRunningState}, {@link ToolCallAuthRequiredState},\n{@link ToolCallPendingResultConfirmationState}, and {@link ToolCallCompletedState}.\n`ToolCallPendingConfirmationState` (not yet confirmed) and\n`ToolCallCancelledState` (the denial path — never ran) don't satisfy this\ninvariant, so they keep their own `selectedOption` field independently\nrather than extending this one.", + "description": "Unstructured terminal output — content before, between, or after commands,\nor from terminals that do not support command detection.", "properties": { - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "type": { + "type": "string", + "enum": [ + "unclassified" + ] }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "value": { + "type": "string", + "description": "Accumulated VT output. Appended to by `terminal/data` when no command is executing." } }, "required": [ - "confirmed" + "type", + "value" ] }, - "ToolCallRunningState": { + "TerminalCommandPart": { "type": "object", - "description": "Tool is actively executing.", + "description": "A single command: its command line and the output it produced.\n\nWhile `isComplete` is false the command is still executing; `output` grows\nas `terminal/data` actions arrive. At `terminal/commandFinished` the part\nis mutated in-place with `isComplete: true` and the completion metadata.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" - }, - "intention": { + "type": { "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "enum": [ + "command" + ] }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "commandId": { + "type": "string", + "description": "Stable id matching the `commandId` on the corresponding\n`terminal/commandExecuted` and `terminal/commandFinished` actions." }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "commandLine": { + "type": "string", + "description": "The command line submitted to the shell." }, - "toolInput": { - "$ref": "#/$defs/ToolInput", - "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + "output": { + "type": "string", + "description": "Accumulated VT output. Appended to by `terminal/data` while `isComplete`\nis false. Shell integration escape sequences are stripped by the server." }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "timestamp": { + "type": "number", + "description": "Unix timestamp (ms) when execution started, as reported by the server." }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "isComplete": { + "type": "boolean", + "description": "Whether the command has finished." }, - "status": { - "const": "running" + "exitCode": { + "type": "number", + "description": "Shell exit code. Set at completion. `undefined` if unknown." }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + "durationMs": { + "type": "number", + "description": "Wall-clock duration in milliseconds. Set at completion." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "confirmed", - "status" + "type", + "commandId", + "commandLine", + "output", + "timestamp", + "isComplete" ] }, - "ToolCallAuthRequiredState": { + "Changeset": { "type": "object", - "description": "A running tool call is paused because the MCP server backing it needs\nauthentication — most commonly {@link McpAuthRequirement.reason |\n`insufficientScope`} step-up auth triggered by the `tools/call` request\nitself. Only ever reached from {@link ToolCallRunningState}, and normally\nreturns there once authenticated: `running` → `auth-required` → `running`\n→ …. A client MAY instead cancel the invocation without authenticating by\ndispatching a `chat/toolCallComplete` with a **failed** result, always\nmoving straight to {@link ToolCallCompletedState} —\n`requiresResultConfirmation` is ignored on this path, so it can never\nenter {@link ToolCallPendingResultConfirmationState}. A **successful**\nresult dispatched from this state is invalid and MUST be rejected/ignored\nas a no-op by the reducer, since execution never resumed after the\nchallenge.\n\nThis is the tool-call-level counterpart to\n{@link McpServerAuthRequiredState} — that state means the MCP *server*\ncannot serve any request; this one means *this specific invocation* is\nwaiting on the same kind of challenge. The two are dispatched\nindependently and MAY be true at the same time, or not: an\n`insufficientScope` challenge triggered by a single tool call, for\nexample, need not block the whole server.\n\nBecause the challenge is always resolved by pushing a token via the\nexisting `authenticate` command, this state can only originate from a\ntool call {@link ToolCallContributorKind.MCP | contributed by an MCP\nserver} — `contributor` is narrowed accordingly (unlike the optional,\nmulti-kind `contributor` on other tool call states).", + "description": "Catalogue entry describing one changeset the server can produce for a\nsession.\n\nCatalogue entries are intentionally lightweight — just enough to render a\nchip or list row without subscribing. Full per-changeset detail\n({@link ChangesetState}) lives on the subscribable URI obtained by\nexpanding {@link uriTemplate}.", "properties": { - "toolCallId": { + "label": { "type": "string", - "description": "Unique tool call identifier" + "description": "Human-readable label, e.g. `\"Uncommitted Changes\"`." }, - "toolName": { + "uriTemplate": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "RFC 6570 URI template. Clients parse the variables directly out of the\ntemplate using the standard `{name}` syntax — they are not redeclared\nhere.\n\nOnly the following template shapes are defined by this protocol; any\nother variable name MUST be ignored by clients (there is no\nprotocol-defined way to obtain values for unknown variables):\n\n| Variables in template | Meaning |\n| ------------------------------------------- | ------------------------------------------------------------------------------------ |\n| _(none)_ | A static, session-wide changeset. The template is itself a subscribable URI. |\n| `{turnId}` | Per-turn slice. Expand with a `Turn.id` from the session. |\n| `{originalTurnId}` and `{modifiedTurnId}` | Diff between two turns. Both variables MUST be present. |\n\nFuture protocol versions MAY add new well-known variables." }, - "displayName": { + "description": { "type": "string", - "description": "Human-readable tool name" + "description": "Optional longer description." }, - "intention": { + "changeKind": { "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" - }, - "contributor": { - "$ref": "#/$defs/ToolCallMcpContributor", - "description": "The MCP server that contributed this tool call — always MCP, never a client tool." + "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." }, - "_meta": { + "capabilities": { + "$ref": "#/$defs/ChangesetCapabilities", + "description": "Optional capability declarations for this changeset. Absent (or an empty\nobject) means the changeset advertises no optional capabilities.\n\nBecause the catalogue entry is delivered up-front on\n{@link ChangesetState | the session's changeset list}, clients can decide\nwhether to surface capability-gated UI (such as review checkboxes) without\nfirst subscribing to the changeset URI. Mirrors the presence-flag\nconvention of `ClientCapabilities`." + } + }, + "required": [ + "label", + "uriTemplate", + "changeKind" + ] + }, + "ChangesetCapabilities": { + "type": "object", + "description": "Optional capabilities a changeset advertises on its catalogue\n{@link Changeset} entry.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities are\nreserved for future per-capability options.", + "properties": { + "review": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" - }, - "toolInput": { - "$ref": "#/$defs/ToolInput", - "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" - }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" - }, + "description": "The changeset supports the per-file **review** workflow. When declared,\nclients MAY surface a GitHub-style \"Viewed\" toggle per file and dispatch\n{@link ChangesetFilesReviewChangedAction | `changeset/filesReviewChanged`} to\nset each file's {@link ChangesetFile.reviewed} flag. Clients that omit\nhandling MUST treat the changeset as non-reviewable." + } + } + }, + "ChangesetState": { + "type": "object", + "description": "Full state for a single changeset, returned when a client subscribes to\nan expanded changeset URI.\n\nThe client already knows the URI it subscribed to, so this state does\nnot redundantly carry it (or the catalogue's `id`, `label`, etc.).\nAggregate counts (`additions`, `deletions`, `files`) are likewise\nomitted: clients trivially compute them from `files[].edit.diff`.", + "properties": { "status": { - "const": "auth-required" + "$ref": "#/$defs/ChangesetStatus", + "description": "Computation lifecycle." }, - "auth": { - "$ref": "#/$defs/McpAuthRequirement", - "description": "The authentication challenge blocking this invocation." + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Present iff `status === ChangesetStatus.Error`." }, - "content": { + "files": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChangesetFile" }, - "description": "Partial content produced before the call paused for authentication." + "description": "Files in this changeset, keyed by {@link ChangesetFile.id}." + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/ChangesetOperation" + }, + "description": "Operations the client may invoke against this changeset. Omit when no\noperations are available." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "contributor", - "invocationMessage", - "confirmed", "status", - "auth" + "files" ] }, - "ToolCallPendingResultConfirmationState": { + "ChangesetFile": { "type": "object", - "description": "Tool finished executing, waiting for client to approve the result.", + "description": "One file entry within a {@link ChangesetState}.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { + "id": { "type": "string", - "description": "Human-readable tool name" + "description": "Stable identifier within the changeset. Typically `after.uri`\n(or `before.uri` for deletions)." }, - "intention": { - "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" + "edit": { + "$ref": "#/$defs/FileEdit", + "description": "Reuses the existing {@link FileEdit} shape. Clients derive line\nadditions, deletions, and rename/create/delete semantics from this." }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "reviewed": { + "type": "boolean", + "description": "Whether a reviewer has marked this file as reviewed (the GitHub-style\n\"Viewed\" checkbox). Absent is equivalent to `false` — clients MUST treat\na missing value as not-yet-reviewed.\n\nRequires the changeset to advertise {@link ChangesetCapabilities.review}.\nClients toggle it by dispatching\n{@link ChangesetFilesReviewChangedAction | `changeset/filesReviewChanged`};\nthe server MAY also originate it (e.g. an agent self-reviewing its own\noutput).\n\nThere is no content version in the protocol, so review is **not** reset\nautomatically when a file's contents change under a stable id. The server,\nwhich is the authority on what changed, resets review explicitly — either\nby re-emitting the file (via {@link ChangesetFileSetAction} or\n{@link ChangesetContentChangedAction}) without `reviewed: true`, or by\ndispatching `changeset/filesReviewChanged` with `reviewed: false`." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" - }, - "toolInput": { - "$ref": "#/$defs/ToolInput", - "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + "description": "Server-defined opaque metadata, surfaced to operations and tooling\nbut not interpreted by the protocol." + } + }, + "required": [ + "id", + "edit" + ] + }, + "ChangesetOperation": { + "type": "object", + "description": "A server-declared invokable verb the client can run against a\nchangeset, a file, or a range — `\"stage\"`, `\"revert\"`, `\"create-pr\"`,\nand so on.\n\nThe term \"operation\" is used deliberately to avoid colliding with the\nprotocol-level [Actions](/guide/actions) that mutate state.", + "properties": { + "id": { + "type": "string", + "description": "Stable identifier, unique within this changeset." }, - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "label": { + "type": "string", + "description": "Human-readable button/menu label." }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "description": { + "type": "string", + "description": "Optional longer description shown on hover or in tooltips." }, - "content": { + "scopes": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/ChangesetOperationScope" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "description": "Where this operation can be invoked." }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + "confirmation": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional confirmation prompt to show before invoking. When present,\nthe client MUST display this message to the user (typically in a\nconfirmation dialog) and only invoke the operation after the user\naccepts. The presence of this field also signals that the operation\nis destructive — clients SHOULD style the affirmative button\naccordingly (e.g. with a warning colour)." }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" + "icon": { + "type": "string", + "description": "Optional generic icon hint, e.g. `\"check\"`, `\"trash\"`." }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "group": { + "type": "string", + "description": "Optional group identifier, used to group related operations together." }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "status": { + "$ref": "#/$defs/ChangesetOperationStatus", + "description": "Current execution status. The server sets\n{@link ChangesetOperationStatus.Running | Running} while an invocation\nis in flight, {@link ChangesetOperationStatus.Error | Error} when the\nmost recent invocation failed, and\n{@link ChangesetOperationStatus.Idle | Idle} otherwise.\n\nClients SHOULD reflect this state in the UI — e.g. disabling the\ncontrol or showing a spinner while `Running`, and surfacing\n{@link error} while `Error`." }, - "status": { - "const": "pending-result-confirmation" + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Cause of failure. Present iff\n`status === ChangesetOperationStatus.Error`; otherwise omitted." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "confirmed", + "id", + "label", + "scopes", "status" ] }, - "ToolCallCompletedState": { + "AnnotationsSummary": { "type": "object", - "description": "Tool completed successfully or with an error.", + "description": "Lightweight per-session summary of the annotations channel, surfaced on\n{@link SessionSummary.annotations} so badge UI can render annotation /\nentry counts without subscribing to the channel itself.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" + "resource": { + "$ref": "#/$defs/URI", + "description": "The subscribable annotations channel URI for the owning session\n(typically `ahp-session://annotations`). Surfaced explicitly even\nthough it is derivable from the session URI so badge UI does not need\nto know the derivation rule." }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "annotationCount": { + "type": "number", + "description": "Total number of {@link Annotation} entries in the channel." }, - "displayName": { + "entryCount": { + "type": "number", + "description": "Total number of {@link AnnotationEntry} entries across every annotation." + } + }, + "required": [ + "resource", + "annotationCount", + "entryCount" + ] + }, + "AnnotationsState": { + "type": "object", + "description": "Full state for a session's annotations channel, returned when a client\nsubscribes to an `ahp-session://annotations` URI.", + "properties": { + "annotations": { + "type": "array", + "items": { + "$ref": "#/$defs/Annotation" + }, + "description": "Annotations in this channel, keyed by {@link Annotation.id}." + } + }, + "required": [ + "annotations" + ] + }, + "Annotation": { + "type": "object", + "description": "A conversation anchored to a specific file produced by a specific turn,\noptionally narrowed to a range within that file.\n\n{@link turnId} anchors the annotation to the file versions that turn\nproduced, so a later turn that rewrites the same file does not silently\ninvalidate the annotation's anchor — clients can resolve {@link resource}\nand {@link range} against the turn's changeset. When {@link range} is\nomitted the annotation is anchored to the entire file.\n\nEvery annotation MUST contain at least one {@link AnnotationEntry}. An\n{@link AnnotationsSetAction} that creates an annotation therefore carries\nits mandatory first entry, and removing the last remaining entry collapses\nthe annotation via {@link AnnotationsRemovedAction} rather than leaving an\nempty annotation behind.", + "properties": { + "id": { "type": "string", - "description": "Human-readable tool name" + "description": "Stable identifier within the annotations channel. Assigned by the client\nthat dispatches the creating {@link AnnotationsSetAction}." }, - "intention": { + "turnId": { "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" - }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + "description": "Turn that produced the file versions this annotation is anchored to.\nMatches a {@link Turn.id} on the owning session." }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "resource": { + "$ref": "#/$defs/URI", + "description": "The file the annotation is anchored to." }, - "toolInput": { - "$ref": "#/$defs/ToolInput", - "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + "range": { + "$ref": "#/$defs/TextRange", + "description": "Range within {@link resource} the annotation is anchored to. When\nomitted the annotation is anchored to the entire file." }, - "success": { + "resolved": { "type": "boolean", - "description": "Whether the tool succeeded" - }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "description": "Whether the annotation has been resolved. Newly created annotations are\nalways unresolved (`false`); a client marks an annotation resolved (or\nre-opens it) by dispatching an {@link AnnotationsUpdatedAction} carrying\nthe updated flag (or an {@link AnnotationsSetAction} when replacing the\nwhole annotation)." }, - "content": { + "entries": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/AnnotationEntry" }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + "description": "Entries in this annotation, in dispatch order (oldest first). MUST\ncontain at least one entry." }, - "structuredContent": { + "_meta": { "type": "object", "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } - }, - "required": [ - "message" - ], - "description": "Error details if the tool failed" - }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" - }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" - }, - "status": { - "const": "completed" + "description": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "confirmed", - "status" + "id", + "turnId", + "resource", + "resolved", + "entries" ] }, - "ToolCallCancelledState": { + "AnnotationEntry": { "type": "object", - "description": "Tool call was cancelled before execution.", + "description": "A single entry within an {@link Annotation}.", "properties": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" - }, - "intention": { + "id": { "type": "string", - "description": "Human-readable description of what the tool invocation intends to do" + "description": "Stable identifier within the enclosing annotation. Assigned by the client\nthat dispatches the {@link AnnotationsEntrySetAction} (or the enclosing\n{@link AnnotationsSetAction}) introducing the entry." }, - "contributor": { - "$ref": "#/$defs/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "text": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Entry body. A bare `string` is rendered as plain text; pass\n`{ markdown: \"…\" }` to opt into Markdown rendering. See\n{@link StringOrMarkdown}." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" - }, - "toolInput": { - "$ref": "#/$defs/ToolInput", - "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." + "description": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." + } + }, + "required": [ + "id", + "text" + ] + }, + "TelemetryCapabilities": { + "type": "object", + "description": "OTLP telemetry channels the agent host emits.\n\nEach field, when present, is either a literal channel URI or an\n[RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) URI template\na client expands and then subscribes to. Absent fields indicate the host\ndoes not emit that signal.\n\nChannel URIs use the `ahp-otlp:` scheme. The scheme identifies the\nprotocol (OpenTelemetry over AHP) so clients can recognise the channel\ntype by URI alone; the host is free to choose any authority/path that\nmakes sense for its implementation. Clients MUST treat the URI as\nopaque (apart from expanding any well-known template variables defined\nbelow) and subscribe with the resulting concrete URI.\n\nPayloads delivered on these channels are OTLP/JSON values — see\n[opentelemetry-proto](https://github.com/open-telemetry/opentelemetry-proto)\nfor the wire shapes (`ExportLogsServiceRequest`,\n`ExportTraceServiceRequest`, `ExportMetricsServiceRequest`).", + "properties": { + "logs": { + "$ref": "#/$defs/URI", + "description": "Channel URI (or RFC 6570 URI template) for OTLP log records\n(`otlp/exportLogs` notifications).\n\nThe following template variables are defined by this protocol; any\nother variable name MUST be ignored by clients (there is no\nprotocol-defined way to obtain values for unknown variables):\n\n| Variables in template | Meaning |\n| --------------------- | ------------------------------------------------------------------------------------------------------- |\n| _(none)_ | The host does not support subscriber-side severity filtering. The template is itself a subscribable URI. |\n| `{level}` | Minimum OTLP severity to deliver. Expand to one of the [OTLP `SeverityNumber`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) short names (case-insensitive): `trace`, `debug`, `info`, `warn`, `error`, `fatal`. The server delivers log records whose `severityNumber` falls in the corresponding band or above. |\n\nHosts SHOULD honour the expanded `{level}`; clients MUST still filter\ndefensively in case a host ignores the parameter. Hosts that do not\nadvertise `{level}` deliver all severities.\n\nFuture protocol versions MAY add new well-known variables (e.g. scope\nor attribute filters)." }, - "status": { - "const": "cancelled" + "traces": { + "$ref": "#/$defs/URI", + "description": "Channel URI for OTLP spans (`otlp/exportTraces` notifications). No\ntemplate variables are defined by this protocol version." }, - "reason": { - "$ref": "#/$defs/ToolCallCancellationReason", - "description": "Why the tool was cancelled" + "metrics": { + "$ref": "#/$defs/URI", + "description": "Channel URI for OTLP metric data points (`otlp/exportMetrics`\nnotifications). No template variables are defined by this protocol\nversion." + } + } + }, + "ResourceWatchState": { + "type": "object", + "description": "Full state for a single resource watch, returned when a client subscribes\nto an `ahp-resource-watch:` URI.\n\nWatches are otherwise stateless: the watcher exists to deliver\n{@link ResourceWatchChangedAction} events. The state carries only the\ndescriptor of what is being watched so a re-subscribing client can\nrecover the watch configuration after reconnecting.", + "properties": { + "root": { + "$ref": "#/$defs/URI", + "description": "The URI being watched. For recursive watches this is the root of the\nsubtree; for non-recursive watches this is the single file or\ndirectory." }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional message explaining the cancellation" + "recursive": { + "type": "boolean", + "description": "`true` if the watcher reports changes for descendants of `root`;\n`false` if it only reports changes to `root` itself (and, when\n`root` is a directory, its direct children)." }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" + "excludes": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "items" + ], + "description": "Optional glob patterns or paths relative to `root` to exclude from\nchange reporting." }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "includes": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "items" + ], + "description": "Optional glob patterns or paths relative to `root` to restrict\nchange reporting to. Omit to report every change under `root`\nsubject to `excludes`." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "reason" + "root", + "recursive" ] }, - "ToolResultTextContent": { + "ResourceChange": { "type": "object", - "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", + "description": "A single change observed by a resource watcher.", "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "The URI of the resource that changed." + }, "type": { - "const": "text" + "$ref": "#/$defs/ResourceChangeType", + "description": "The kind of change observed." + } + }, + "required": [ + "uri", + "type" + ] + }, + "AutomationLocalTime": { + "type": "object", + "properties": { + "hour": { + "type": "number" }, - "text": { - "type": "string", - "description": "The text content" + "minute": { + "type": "number" } }, "required": [ - "type", - "text" + "hour", + "minute" ] }, - "ToolResultEmbeddedResourceContent": { + "AutomationHourlySchedule": { "type": "object", - "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", "properties": { - "type": { - "const": "embeddedResource" + "kind": { + "const": "hourly" + } + }, + "required": [ + "kind" + ] + }, + "AutomationDailySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "daily" }, - "data": { - "type": "string", - "description": "Base64-encoded data" + "time": { + "$ref": "#/$defs/AutomationLocalTime" }, - "contentType": { + "timeZone": { "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "description": "IANA time-zone identifier." } }, "required": [ - "type", - "data", - "contentType" + "kind", + "time", + "timeZone" ] }, - "ToolResultResourceContent": { + "AutomationWeeklySchedule": { "type": "object", - "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "kind": { + "const": "weekly" }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "weekday": { + "$ref": "#/$defs/AutomationWeekday" }, - "contentType": { + "time": { + "$ref": "#/$defs/AutomationLocalTime" + }, + "timeZone": { "type": "string", - "description": "Content MIME type" + "description": "IANA time-zone identifier." + } + }, + "required": [ + "kind", + "weekday", + "time", + "timeZone" + ] + }, + "AutomationCronSchedule": { + "type": "object", + "properties": { + "kind": { + "const": "cron" }, - "nonce": { + "expression": { "type": "string", - "description": "Content nonce" + "description": "Standard five-field Unix cron expression." }, - "type": { - "const": "resource" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." } }, "required": [ - "uri", - "type" + "kind", + "expression", + "timeZone" ] }, - "ToolResultFileEditContent": { + "AutomationScheduleTrigger": { "type": "object", - "description": "Describes a file modification performed by a tool.", "properties": { - "before": { - "type": "object", - "properties": { - "uri": { - "$ref": "#/$defs/URI" - }, - "content": { - "$ref": "#/$defs/ContentRef" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." + "id": { + "type": "string", + "description": "Stable within the automation definition." }, - "after": { - "type": "object", - "properties": { - "uri": { - "$ref": "#/$defs/URI" - }, - "content": { - "$ref": "#/$defs/ContentRef" - } - }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." + "kind": { + "const": "schedule" }, - "diff": { - "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } - }, - "description": "Optional diff display metadata" + "schedule": { + "$ref": "#/$defs/AutomationSchedule" }, - "type": { - "const": "fileEdit" + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy" } }, "required": [ - "type" + "id", + "kind", + "schedule" ] }, - "ToolResultTerminalContent": { + "AutomationEventTrigger": { "type": "object", - "description": "A reference to a terminal whose output is relevant to this tool result.\n\nClients can subscribe to the terminal's URI to stream its output in real\ntime, providing live feedback while a tool is executing.\n\nWhen the command exits, {@link result} is filled in on the completed\nresult, retaining the outcome for clients that did not subscribe. This\nrecords the command's exit, not the terminal's — the terminal may keep\nrunning afterwards.", "properties": { - "type": { - "const": "terminal" + "id": { + "type": "string", + "description": "Stable within the automation definition." }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "kind": { + "const": "event" }, - "title": { + "type": { "type": "string", - "description": "Display title for the terminal content" + "description": "Stable host-defined trigger type." }, - "isPty": { - "type": "boolean", - "description": "Whether this terminal-style resource is backed by a pseudoterminal.\nWhen `false`, output is plain text and clients do not need to parse\nVT sequences." + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selected event actions." }, - "result": { - "$ref": "#/$defs/TerminalCommandResult", - "description": "Outcome of the command, present once it has exited." + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Schema-defined values. Unknown entries must survive round-trips." } }, "required": [ + "id", + "kind", "type", - "resource", - "title" + "events" ] }, - "TerminalCommandResult": { + "AutomationTriggerEventDefinition": { "type": "object", - "description": "Outcome of a command run in a terminal-style tool, filled in on\n{@link ToolResultTerminalContent.result} once the command exits.", "properties": { - "exitCode": { - "type": "number", - "description": "Exit code from the completed command, if reported by the runtime" + "id": { + "type": "string" }, - "preview": { - "type": "string", - "description": "Preview of the command's output, for clients that are not subscribed\nto the terminal or that arrive after it is disposed. When `isPty` is\n`true` the preview may contain VT sequences; when `false` it is plain\ntext." + "title": { + "type": "string" }, - "truncated": { - "type": "boolean", - "description": "Whether `preview` is known to be incomplete or truncated" + "description": { + "type": "string" } - } + }, + "required": [ + "id", + "title" + ] }, - "ToolResultSubagentContent": { + "AutomationTriggerDefinition": { "type": "object", - "description": "A reference, embedded in a tool result, to a worker chat spawned by the tool\ncall (a sub-agent delegation), referenced by a chat URI (`ahp-chat:/...`).\n\nThis is the spawning tool call's forward view of the worker. The worker chat\nrecords the same edge in reverse via its {@link ChatOrigin} (`kind: 'tool'`),\nwhose `toolCallId` identifies the tool call that emitted this content.", "properties": { "type": { - "const": "subagent" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Worker chat URI (subscribable for full chat state)" + "type": "string" }, "title": { - "type": "string", - "description": "Display title for the subagent" - }, - "agentName": { - "type": "string", - "description": "Internal agent name" + "type": "string" }, "description": { - "type": "string", - "description": "Human-readable description of the subagent's task" + "type": "string" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTriggerEventDefinition" + } + }, + "configSchema": { + "$ref": "#/$defs/ConfigSchema" } }, "required": [ "type", - "resource", - "title" + "title", + "events" ] }, - "TerminalInfo": { + "AutomationSessionTemplate": { "type": "object", - "description": "Lightweight terminal metadata exposed on the root state.", "properties": { - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "provider": { + "type": "string" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Absence means a workspace-less session." }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values resolved through `resolveSessionConfig`." + } + } + }, + "AutomationDefinition": { + "type": "object", + "properties": { "title": { - "type": "string", - "description": "Human-readable terminal title" + "type": "string" }, - "claim": { - "$ref": "#/$defs/TerminalClaim", - "description": "Who currently holds this terminal" + "message": { + "$ref": "#/$defs/Message", + "description": "Initial user message sent to each new session." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate" + }, + "enabled": { + "type": "boolean", + "description": "Controls automatic triggers; manual runs remain permitted." + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + }, + "description": "Empty means manual-only." }, - "exitCode": { - "type": "number", - "description": "Process exit code, if the terminal process has exited" + "_meta": { + "type": "object", + "additionalProperties": {} } }, "required": [ - "resource", "title", - "claim" + "message", + "session", + "enabled", + "triggers" ] }, - "TerminalClientClaim": { + "AutomationRuntimeState": { "type": "object", - "description": "A terminal claimed by a connected client.", "properties": { - "kind": { - "const": "client", - "description": "Discriminant" + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } }, - "clientId": { - "type": "string", - "description": "The `clientId` of the claiming client" + "_meta": { + "type": "object", + "additionalProperties": {} } - }, - "required": [ - "kind", - "clientId" - ] + } }, - "TerminalSessionClaim": { + "AutomationSummary": { "type": "object", - "description": "A terminal claimed by a session, optionally scoped to a specific turn or tool call.", "properties": { - "kind": { - "const": "session", - "description": "Discriminant" + "resource": { + "$ref": "#/$defs/URI" }, - "session": { - "$ref": "#/$defs/URI", - "description": "Session URI that claimed the terminal" + "title": { + "type": "string" }, - "turnId": { - "type": "string", - "description": "Optional turn identifier within the session" + "enabled": { + "type": "boolean" }, - "toolCallId": { - "type": "string", - "description": "Optional tool call identifier within the turn" + "triggerCount": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "lastRun": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "revision": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} } }, "required": [ - "kind", - "session" + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" ] }, - "TerminalState": { + "AutomationState": { "type": "object", - "description": "Full state for a single terminal, loaded when a client subscribes to the terminal's URI.", "properties": { - "title": { - "type": "string", - "description": "Human-readable terminal title" + "resource": { + "$ref": "#/$defs/URI" }, - "cwd": { - "$ref": "#/$defs/URI", - "description": "Current working directory of the terminal process" + "definition": { + "$ref": "#/$defs/AutomationDefinition" }, - "cols": { - "type": "number", - "description": "Terminal width in columns" + "revision": { + "type": "number" }, - "rows": { - "type": "number", - "description": "Terminal height in rows" + "nextRunAt": { + "type": "string" }, - "content": { + "runs": { "type": "array", "items": { - "$ref": "#/$defs/TerminalContentPart" + "$ref": "#/$defs/AutomationRunSummary" }, - "description": "Typed content parts, replacing the flat `content: string`.\n\nNaive consumers that only need the raw VT stream can reconstruct it with:\n `content.map(p => p.type === 'command' ? p.output : p.value).join('')`\n\nConsumers that need command boundaries can filter by part type." + "description": "Newest-first retained run summaries." }, - "exitCode": { - "type": "number", - "description": "Process exit code, set when the terminal process exits" + "runsNextCursor": { + "type": "string" }, - "claim": { - "$ref": "#/$defs/TerminalClaim", - "description": "Who currently holds this terminal" + "runtime": { + "$ref": "#/$defs/AutomationRuntimeState" }, - "supportsCommandDetection": { - "type": "boolean", - "description": "Whether this terminal emits `terminal/commandExecuted` and\n`terminal/commandFinished` actions and populates `command`-typed parts.\n\nClients MUST check this flag before relying on command detection.\nDo NOT use the presence of a `command` part as a feature flag — parts\nare absent in the normal idle state." + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } }, - "isPty": { - "type": "boolean", - "description": "Whether this terminal-style resource is backed by a pseudoterminal.\nWhen `false`, output is plain text and clients do not need to parse\nVT sequences." + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} } }, "required": [ - "title", - "content", - "claim" + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" ] }, - "TerminalUnclassifiedPart": { + "AutomationRunBlocker": { "type": "object", - "description": "Unstructured terminal output — content before, between, or after commands,\nor from terminals that do not support command detection.", "properties": { - "type": { - "type": "string", - "enum": [ - "unclassified" - ] - }, - "value": { - "type": "string", - "description": "Accumulated VT output. Appended to by `terminal/data` when no command is executing." + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind" } }, "required": [ - "type", - "value" + "kind" ] }, - "TerminalCommandPart": { + "AutomationManualRunCause": { "type": "object", - "description": "A single command: its command line and the output it produced.\n\nWhile `isComplete` is false the command is still executing; `output` grows\nas `terminal/data` actions arrive. At `terminal/commandFinished` the part\nis mutated in-place with `isComplete: true` and the completion metadata.", "properties": { - "type": { - "type": "string", - "enum": [ - "command" - ] - }, - "commandId": { - "type": "string", - "description": "Stable id matching the `commandId` on the corresponding\n`terminal/commandExecuted` and `terminal/commandFinished` actions." - }, - "commandLine": { - "type": "string", - "description": "The command line submitted to the shell." - }, - "output": { - "type": "string", - "description": "Accumulated VT output. Appended to by `terminal/data` while `isComplete`\nis false. Shell integration escape sequences are stripped by the server." - }, - "timestamp": { - "type": "number", - "description": "Unix timestamp (ms) when execution started, as reported by the server." - }, - "isComplete": { - "type": "boolean", - "description": "Whether the command has finished." - }, - "exitCode": { - "type": "number", - "description": "Shell exit code. Set at completion. `undefined` if unknown." - }, - "durationMs": { - "type": "number", - "description": "Wall-clock duration in milliseconds. Set at completion." + "kind": { + "const": "manual" } }, "required": [ - "type", - "commandId", - "commandLine", - "output", - "timestamp", - "isComplete" + "kind" ] }, - "Changeset": { + "AutomationTriggeredRunCause": { "type": "object", - "description": "Catalogue entry describing one changeset the server can produce for a\nsession.\n\nCatalogue entries are intentionally lightweight — just enough to render a\nchip or list row without subscribing. Full per-changeset detail\n({@link ChangesetState}) lives on the subscribable URI obtained by\nexpanding {@link uriTemplate}.", "properties": { - "label": { - "type": "string", - "description": "Human-readable label, e.g. `\"Uncommitted Changes\"`." + "kind": { + "const": "trigger" }, - "uriTemplate": { - "type": "string", - "description": "RFC 6570 URI template. Clients parse the variables directly out of the\ntemplate using the standard `{name}` syntax — they are not redeclared\nhere.\n\nOnly the following template shapes are defined by this protocol; any\nother variable name MUST be ignored by clients (there is no\nprotocol-defined way to obtain values for unknown variables):\n\n| Variables in template | Meaning |\n| ------------------------------------------- | ------------------------------------------------------------------------------------ |\n| _(none)_ | A static, session-wide changeset. The template is itself a subscribable URI. |\n| `{turnId}` | Per-turn slice. Expand with a `Turn.id` from the session. |\n| `{originalTurnId}` and `{modifiedTurnId}` | Diff between two turns. Both variables MUST be present. |\n\nFuture protocol versions MAY add new well-known variables." + "triggerId": { + "type": "string" }, - "description": { - "type": "string", - "description": "Optional longer description." + "scheduledFor": { + "type": "string" }, - "changeKind": { - "type": "string", - "description": "Advisory hint describing what kind of changeset this is, so clients can\ngroup, sort, or render an appropriate icon without parsing\n{@link uriTemplate}. Recognized values include:\n\n- `'session'`: a static, session-wide changeset covering all changes the\n agent has produced in this session.\n- `'branch'`: changes relative to a base branch (e.g. a feature branch\n diffed against `main`).\n- `'uncommitted'`: the workspace's current uncommitted changes.\n- `'turn'`: changes produced by a single turn. Typically paired with a\n `{turnId}` variable in {@link uriTemplate}.\n- `'compare-turns'`: a diff between two turns. Typically paired with\n `{originalTurnId}` and `{modifiedTurnId}` variables in\n {@link uriTemplate}.\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "catchUp": { + "type": "boolean" }, - "capabilities": { - "$ref": "#/$defs/ChangesetCapabilities", - "description": "Optional capability declarations for this changeset. Absent (or an empty\nobject) means the changeset advertises no optional capabilities.\n\nBecause the catalogue entry is delivered up-front on\n{@link ChangesetState | the session's changeset list}, clients can decide\nwhether to surface capability-gated UI (such as review checkboxes) without\nfirst subscribing to the changeset URI. Mirrors the presence-flag\nconvention of `ClientCapabilities`." + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined event provenance containing no secrets." } }, "required": [ - "label", - "uriTemplate", - "changeKind" + "kind", + "triggerId" ] }, - "ChangesetCapabilities": { + "AutomationPendingRunLifecycle": { "type": "object", - "description": "Optional capabilities a changeset advertises on its catalogue\n{@link Changeset} entry.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities are\nreserved for future per-capability options.", "properties": { - "review": { - "type": "object", - "additionalProperties": {}, - "description": "The changeset supports the per-file **review** workflow. When declared,\nclients MAY surface a GitHub-style \"Viewed\" toggle per file and dispatch\n{@link ChangesetFilesReviewChangedAction | `changeset/filesReviewChanged`} to\nset each file's {@link ChangesetFile.reviewed} flag. Clients that omit\nhandling MUST treat the changeset as non-reviewable." + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string" } - } + }, + "required": [ + "status", + "createdAt" + ] }, - "ChangesetState": { + "AutomationRunningRunLifecycle": { "type": "object", - "description": "Full state for a single changeset, returned when a client subscribes to\nan expanded changeset URI.\n\nThe client already knows the URI it subscribed to, so this state does\nnot redundantly carry it (or the catalogue's `id`, `label`, etc.).\nAggregate counts (`additions`, `deletions`, `files`) are likewise\nomitted: clients trivially compute them from `files[].edit.diff`.", "properties": { "status": { - "$ref": "#/$defs/ChangesetStatus", - "description": "Computation lifecycle." - }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Present iff `status === ChangesetStatus.Error`." + "const": "running" }, - "files": { - "type": "array", - "items": { - "$ref": "#/$defs/ChangesetFile" - }, - "description": "Files in this changeset, keyed by {@link ChangesetFile.id}." + "createdAt": { + "type": "string" }, - "operations": { - "type": "array", - "items": { - "$ref": "#/$defs/ChangesetOperation" - }, - "description": "Operations the client may invoke against this changeset. Omit when no\noperations are available." + "startedAt": { + "type": "string" } }, "required": [ "status", - "files" + "createdAt", + "startedAt" ] }, - "ChangesetFile": { + "AutomationBlockedRunLifecycle": { "type": "object", - "description": "One file entry within a {@link ChangesetState}.", "properties": { - "id": { - "type": "string", - "description": "Stable identifier within the changeset. Typically `after.uri`\n(or `before.uri` for deletions)." + "status": { + "const": "blocked" }, - "edit": { - "$ref": "#/$defs/FileEdit", - "description": "Reuses the existing {@link FileEdit} shape. Clients derive line\nadditions, deletions, and rename/create/delete semantics from this." + "createdAt": { + "type": "string" }, - "reviewed": { - "type": "boolean", - "description": "Whether a reviewer has marked this file as reviewed (the GitHub-style\n\"Viewed\" checkbox). Absent is equivalent to `false` — clients MUST treat\na missing value as not-yet-reviewed.\n\nRequires the changeset to advertise {@link ChangesetCapabilities.review}.\nClients toggle it by dispatching\n{@link ChangesetFilesReviewChangedAction | `changeset/filesReviewChanged`};\nthe server MAY also originate it (e.g. an agent self-reviewing its own\noutput).\n\nThere is no content version in the protocol, so review is **not** reset\nautomatically when a file's contents change under a stable id. The server,\nwhich is the authority on what changed, resets review explicitly — either\nby re-emitting the file (via {@link ChangesetFileSetAction} or\n{@link ChangesetContentChangedAction}) without `reviewed: true`, or by\ndispatching `changeset/filesReviewChanged` with `reviewed: false`." + "startedAt": { + "type": "string" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Server-defined opaque metadata, surfaced to operations and tooling\nbut not interpreted by the protocol." + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker" } }, "required": [ - "id", - "edit" + "status", + "createdAt", + "startedAt", + "blocker" ] }, - "ChangesetOperation": { + "AutomationCompletedRunLifecycle": { "type": "object", - "description": "A server-declared invokable verb the client can run against a\nchangeset, a file, or a range — `\"stage\"`, `\"revert\"`, `\"create-pr\"`,\nand so on.\n\nThe term \"operation\" is used deliberately to avoid colliding with the\nprotocol-level [Actions](/guide/actions) that mutate state.", "properties": { - "id": { - "type": "string", - "description": "Stable identifier, unique within this changeset." - }, - "label": { - "type": "string", - "description": "Human-readable button/menu label." - }, - "description": { - "type": "string", - "description": "Optional longer description shown on hover or in tooltips." - }, - "scopes": { - "type": "array", - "items": { - "$ref": "#/$defs/ChangesetOperationScope" - }, - "description": "Where this operation can be invoked." - }, - "confirmation": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional confirmation prompt to show before invoking. When present,\nthe client MUST display this message to the user (typically in a\nconfirmation dialog) and only invoke the operation after the user\naccepts. The presence of this field also signals that the operation\nis destructive — clients SHOULD style the affirmative button\naccordingly (e.g. with a warning colour)." + "status": { + "const": "completed" }, - "icon": { - "type": "string", - "description": "Optional generic icon hint, e.g. `\"check\"`, `\"trash\"`." + "createdAt": { + "type": "string" }, - "group": { - "type": "string", - "description": "Optional group identifier, used to group related operations together." + "startedAt": { + "type": "string" }, - "status": { - "$ref": "#/$defs/ChangesetOperationStatus", - "description": "Current execution status. The server sets\n{@link ChangesetOperationStatus.Running | Running} while an invocation\nis in flight, {@link ChangesetOperationStatus.Error | Error} when the\nmost recent invocation failed, and\n{@link ChangesetOperationStatus.Idle | Idle} otherwise.\n\nClients SHOULD reflect this state in the UI — e.g. disabling the\ncontrol or showing a spinner while `Running`, and surfacing\n{@link error} while `Error`." + "completedAt": { + "type": "string" }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Cause of failure. Present iff\n`status === ChangesetOperationStatus.Error`; otherwise omitted." + "usage": { + "$ref": "#/$defs/UsageInfo" } }, "required": [ - "id", - "label", - "scopes", - "status" + "status", + "createdAt", + "startedAt", + "completedAt" ] }, - "AnnotationsSummary": { + "AutomationFailedRunLifecycle": { "type": "object", - "description": "Lightweight per-session summary of the annotations channel, surfaced on\n{@link SessionSummary.annotations} so badge UI can render annotation /\nentry counts without subscribing to the channel itself.", "properties": { - "resource": { - "$ref": "#/$defs/URI", - "description": "The subscribable annotations channel URI for the owning session\n(typically `ahp-session://annotations`). Surfaced explicitly even\nthough it is derivable from the session URI so badge UI does not need\nto know the derivation rule." + "status": { + "const": "failed" }, - "annotationCount": { - "type": "number", - "description": "Total number of {@link Annotation} entries in the channel." + "createdAt": { + "type": "string" }, - "entryCount": { - "type": "number", - "description": "Total number of {@link AnnotationEntry} entries across every annotation." + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "error": { + "$ref": "#/$defs/ErrorInfo" } }, "required": [ - "resource", - "annotationCount", - "entryCount" + "status", + "createdAt", + "completedAt", + "error" ] }, - "AnnotationsState": { + "AutomationCancelledRunLifecycle": { "type": "object", - "description": "Full state for a session's annotations channel, returned when a client\nsubscribes to an `ahp-session://annotations` URI.", "properties": { - "annotations": { - "type": "array", - "items": { - "$ref": "#/$defs/Annotation" - }, - "description": "Annotations in this channel, keyed by {@link Annotation.id}." + "status": { + "const": "cancelled" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" } }, "required": [ - "annotations" + "status", + "createdAt", + "completedAt" ] }, - "Annotation": { + "AutomationRunArtifact": { "type": "object", - "description": "A conversation anchored to a specific file produced by a specific turn,\noptionally narrowed to a range within that file.\n\n{@link turnId} anchors the annotation to the file versions that turn\nproduced, so a later turn that rewrites the same file does not silently\ninvalidate the annotation's anchor — clients can resolve {@link resource}\nand {@link range} against the turn's changeset. When {@link range} is\nomitted the annotation is anchored to the entire file.\n\nEvery annotation MUST contain at least one {@link AnnotationEntry}. An\n{@link AnnotationsSetAction} that creates an annotation therefore carries\nits mandatory first entry, and removing the last remaining entry collapses\nthe annotation via {@link AnnotationsRemovedAction} rather than leaving an\nempty annotation behind.", "properties": { - "id": { - "type": "string", - "description": "Stable identifier within the annotations channel. Assigned by the client\nthat dispatches the creating {@link AnnotationsSetAction}." - }, - "turnId": { - "type": "string", - "description": "Turn that produced the file versions this annotation is anchored to.\nMatches a {@link Turn.id} on the owning session." - }, - "resource": { + "uri": { "$ref": "#/$defs/URI", - "description": "The file the annotation is anchored to." + "description": "Content URI" }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Range within {@link resource} the annotation is anchored to. When\nomitted the annotation is anchored to the entire file." + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "resolved": { - "type": "boolean", - "description": "Whether the annotation has been resolved. Newly created annotations are\nalways unresolved (`false`); a client marks an annotation resolved (or\nre-opens it) by dispatching an {@link AnnotationsUpdatedAction} carrying\nthe updated flag (or an {@link AnnotationsSetAction} when replacing the\nwhole annotation)." + "contentType": { + "type": "string", + "description": "Content MIME type" }, - "entries": { - "type": "array", - "items": { - "$ref": "#/$defs/AnnotationEntry" - }, - "description": "Entries in this annotation, in dispatch order (oldest first). MUST\ncontain at least one entry." + "nonce": { + "type": "string", + "description": "Content nonce" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." - } - }, - "required": [ - "id", - "turnId", - "resource", - "resolved", - "entries" - ] - }, - "AnnotationEntry": { - "type": "object", - "description": "A single entry within an {@link Annotation}.", - "properties": { "id": { - "type": "string", - "description": "Stable identifier within the enclosing annotation. Assigned by the client\nthat dispatches the {@link AnnotationsEntrySetAction} (or the enclosing\n{@link AnnotationsSetAction}) introducing the entry." + "type": "string" }, - "text": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Entry body. A bare `string` is rendered as plain text; pass\n`{ markdown: \"…\" }` to opt into Markdown rendering. See\n{@link StringOrMarkdown}." + "label": { + "type": "string" }, "_meta": { "type": "object", - "additionalProperties": {}, - "description": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." + "additionalProperties": {} } }, "required": [ + "uri", "id", - "text" + "label" ] }, - "TelemetryCapabilities": { + "AutomationRunSummary": { "type": "object", - "description": "OTLP telemetry channels the agent host emits.\n\nEach field, when present, is either a literal channel URI or an\n[RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) URI template\na client expands and then subscribes to. Absent fields indicate the host\ndoes not emit that signal.\n\nChannel URIs use the `ahp-otlp:` scheme. The scheme identifies the\nprotocol (OpenTelemetry over AHP) so clients can recognise the channel\ntype by URI alone; the host is free to choose any authority/path that\nmakes sense for its implementation. Clients MUST treat the URI as\nopaque (apart from expanding any well-known template variables defined\nbelow) and subscribe with the resulting concrete URI.\n\nPayloads delivered on these channels are OTLP/JSON values — see\n[opentelemetry-proto](https://github.com/open-telemetry/opentelemetry-proto)\nfor the wire shapes (`ExportLogsServiceRequest`,\n`ExportTraceServiceRequest`, `ExportMetricsServiceRequest`).", "properties": { - "logs": { - "$ref": "#/$defs/URI", - "description": "Channel URI (or RFC 6570 URI template) for OTLP log records\n(`otlp/exportLogs` notifications).\n\nThe following template variables are defined by this protocol; any\nother variable name MUST be ignored by clients (there is no\nprotocol-defined way to obtain values for unknown variables):\n\n| Variables in template | Meaning |\n| --------------------- | ------------------------------------------------------------------------------------------------------- |\n| _(none)_ | The host does not support subscriber-side severity filtering. The template is itself a subscribable URI. |\n| `{level}` | Minimum OTLP severity to deliver. Expand to one of the [OTLP `SeverityNumber`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber) short names (case-insensitive): `trace`, `debug`, `info`, `warn`, `error`, `fatal`. The server delivers log records whose `severityNumber` falls in the corresponding band or above. |\n\nHosts SHOULD honour the expanded `{level}`; clients MUST still filter\ndefensively in case a host ignores the parameter. Hosts that do not\nadvertise `{level}` deliver all severities.\n\nFuture protocol versions MAY add new well-known variables (e.g. scope\nor attribute filters)." + "resource": { + "$ref": "#/$defs/URI" }, - "traces": { - "$ref": "#/$defs/URI", - "description": "Channel URI for OTLP spans (`otlp/exportTraces` notifications). No\ntemplate variables are defined by this protocol version." + "automation": { + "$ref": "#/$defs/URI" }, - "metrics": { - "$ref": "#/$defs/URI", - "description": "Channel URI for OTLP metric data points (`otlp/exportMetrics`\nnotifications). No template variables are defined by this protocol\nversion." - } - } - }, - "ResourceWatchState": { - "type": "object", - "description": "Full state for a single resource watch, returned when a client subscribes\nto an `ahp-resource-watch:` URI.\n\nWatches are otherwise stateless: the watcher exists to deliver\n{@link ResourceWatchChangedAction} events. The state carries only the\ndescriptor of what is being watched so a re-subscribing client can\nrecover the watch configuration after reconnecting.", - "properties": { - "root": { - "$ref": "#/$defs/URI", - "description": "The URI being watched. For recursive watches this is the root of the\nsubtree; for non-recursive watches this is the single file or\ndirectory." + "cause": { + "$ref": "#/$defs/AutomationRunCause" }, - "recursive": { - "type": "boolean", - "description": "`true` if the watcher reports changes for descendants of `root`;\n`false` if it only reports changes to `root` itself (and, when\n`root` is a directory, its direct children)." + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" }, - "excludes": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "items" - ], - "description": "Optional glob patterns or paths relative to `root` to exclude from\nchange reporting." + "primarySession": { + "$ref": "#/$defs/URI" }, - "includes": { + "sessionCount": { + "type": "number" + }, + "artifactCount": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "items" - ], - "description": "Optional glob patterns or paths relative to `root` to restrict\nchange reporting to. Omit to report every change under `root`\nsubject to `excludes`." + "additionalProperties": {} } }, "required": [ - "root", - "recursive" + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" ] }, - "ResourceChange": { + "AutomationRunState": { "type": "object", - "description": "A single change observed by a resource watcher.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "The URI of the resource that changed." + "resource": { + "$ref": "#/$defs/URI" }, - "type": { - "$ref": "#/$defs/ResourceChangeType", - "description": "The kind of change observed." + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "sessions": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "artifacts": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} } }, "required": [ - "uri", - "type" + "resource", + "automation", + "cause", + "lifecycle", + "sessions", + "artifacts", + "operations" ] }, "ActionOrigin": { @@ -8465,10 +9529,210 @@ "changes" ] }, + "AutomationDefinitionChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/definitionChanged" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "revision": { + "type": "number" + }, + "modifiedAt": { + "type": "string" + }, + "nextRunAt": { + "type": "string" + } + }, + "required": [ + "type", + "definition", + "revision", + "modifiedAt" + ] + }, + "AutomationRunSummarySetAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runSummarySet" + }, + "run": { + "$ref": "#/$defs/AutomationRunSummary" + } + }, + "required": [ + "type", + "run" + ] + }, + "AutomationRunSummaryRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runSummaryRemoved" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "run" + ] + }, + "AutomationRunsLoadedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runsLoaded" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + } + }, + "nextCursor": { + "type": "string" + } + }, + "required": [ + "type", + "runs" + ] + }, + "AutomationRunLifecycleChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/lifecycleChanged" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + } + }, + "required": [ + "type", + "lifecycle", + "operations" + ] + }, + "AutomationRunSessionSetAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/sessionSet" + }, + "session": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "session" + ] + }, + "AutomationRunSessionRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/sessionRemoved" + }, + "session": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "session" + ] + }, + "AutomationRunPrimarySessionChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/primarySessionChanged" + }, + "primarySession": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type" + ] + }, + "AutomationRunArtifactSetAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/artifactSet" + }, + "artifact": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "required": [ + "type", + "artifact" + ] + }, + "AutomationRunArtifactRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/artifactRemoved" + }, + "artifactId": { + "type": "string" + } + }, + "required": [ + "type", + "artifactId" + ] + }, + "AutomationRunCancelRequestedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/cancelRequested" + } + }, + "required": [ + "type" + ] + }, "URI": { "type": "string", "description": "A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)." }, + "AutomationExecutionLifetime": { + "enum": [ + "hostLifetime", + "managed" + ], + "type": "string" + }, + "AutomationScheduleKind": { + "enum": [ + "hourly", + "daily", + "weekly", + "cron" + ], + "type": "string" + }, "StateAction": { "oneOf": [ { @@ -8725,6 +9989,39 @@ }, { "$ref": "#/$defs/ResourceWatchChangedAction" + }, + { + "$ref": "#/$defs/AutomationDefinitionChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunSummarySetAction" + }, + { + "$ref": "#/$defs/AutomationRunSummaryRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunsLoadedAction" + }, + { + "$ref": "#/$defs/AutomationRunLifecycleChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunSessionSetAction" + }, + { + "$ref": "#/$defs/AutomationRunSessionRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunPrimarySessionChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunArtifactSetAction" + }, + { + "$ref": "#/$defs/AutomationRunArtifactRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunCancelRequestedAction" } ], "description": "Discriminated union of all state actions." @@ -8876,6 +10173,32 @@ ], "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$ref": "#/$defs/AutomationEventTrigger" + } + ] + }, + "AutomationSchedule": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationHourlySchedule" + }, + { + "$ref": "#/$defs/AutomationDailySchedule" + }, + { + "$ref": "#/$defs/AutomationWeeklySchedule" + }, + { + "$ref": "#/$defs/AutomationCronSchedule" + } + ] + }, "JsonPrimitive": { "oneOf": [ { @@ -8928,6 +10251,9 @@ "type": "number", "description": "Bitset of summary-level session status flags.\n\nUse bitwise checks instead of equality for non-terminal activity. For example,\n`status & SessionStatus.InProgress` matches both ordinary in-progress turns\nand turns that are paused waiting for input." }, + "SessionOrigin": { + "$ref": "#/$defs/AutomationSessionOrigin" + }, "SessionLifecycle": { "enum": [ "creating", @@ -9403,6 +10729,80 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, + "AutomationWeekday": { + "enum": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "type": "string" + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string" + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "type": "string" + }, + "AutomationRunBlockerKind": { + "enum": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "type": "string" + }, + "AutomationRunCause": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ] + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ] + }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string" + }, "PendingMessageKind": { "enum": [ "steering", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index b1b9745fb..44b0c54e9 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -507,6 +507,12 @@ }, { "$ref": "#/$defs/ChatState" + }, + { + "$ref": "#/$defs/AutomationState" + }, + { + "$ref": "#/$defs/AutomationRunState" } ], "description": "The current state of the resource" @@ -734,6 +740,25 @@ "values" ] }, + "AutomationSessionOrigin": { + "type": "object", + "properties": { + "kind": { + "const": "automation" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "kind", + "automation", + "run" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -754,6 +779,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -796,6 +825,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -1100,6 +1133,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -4979,263 +5016,1033 @@ "type" ] }, - "BaseParams": { + "AutomationLocalTime": { "type": "object", - "description": "Base shape every command's params extends.\n\n`channel` identifies the channel the command targets, mirroring the\n`channel` field on every protocol notification. For commands that operate\non a specific channel (a session, terminal, or changeset), `channel` is\nthat channel's URI. For commands that are connection-level rather than\nchannel-scoped (e.g. {@link InitializeParams | `initialize`},\n{@link PingParams | `ping`}, {@link ListSessionsParams | `listSessions`},\nthe `resource*` filesystem commands, and {@link AuthenticateParams |\n`authenticate`}), the params type narrows `channel` to the literal\nroot URI `'ahp-root://'`.\n\nThis invariant lets implementations route every incoming message —\nrequest, response, or notification — by inspecting `params.channel`\nwithout needing to know the per-method param shape.", "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." + "hour": { + "type": "number" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + "minute": { + "type": "number" } }, "required": [ - "channel" + "hour", + "minute" ] }, - "PaginatedParams": { + "AutomationHourlySchedule": { "type": "object", - "description": "Cursor-based pagination inputs, mixed into the params of any list command\nthat can page a large result set (e.g. {@link ListSessionsParams |\n`listSessions`}). The paired output is {@link PaginatedResult}.\n\nPagination is **opaque and cursor-based**, mirroring the shape `fetchTurns`\nalready uses for chat history: the server owns the ordering and keyset, and\nthe client walks pages by echoing the cursor from the previous\n{@link PaginatedResult.nextCursor} back on the next request.\n\nThe contract every paginated command shares:\n\n- To fetch the first page, omit `cursor`. Supply `limit` to bound the page.\n- If the result carries a {@link PaginatedResult.nextCursor}, more entries\n exist — pass it back as `cursor` to fetch the following page. A missing\n `nextCursor` signals the end of the collection.\n- Cursors are **server-defined and opaque**: clients MUST NOT parse, modify,\n or persist them across connections. An unrecognised cursor SHOULD be\n rejected with an `InvalidParams` error.\n- Pagination is **fully additive**: a client that omits `limit`/`cursor` and\n ignores `nextCursor` sees the pre-pagination behaviour (subject to any\n server-imposed cap), and a server that does not paginate ignores the inputs\n and returns everything in a single page.", "properties": { - "limit": { - "type": "number", - "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." - }, - "cursor": { - "type": "string", - "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + "kind": { + "const": "hourly" } - } + }, + "required": [ + "kind" + ] }, - "PaginatedResult": { + "AutomationDailySchedule": { "type": "object", - "description": "Cursor-based pagination output, extended by the result of any list command\nthat can page a large result set (e.g. {@link ListSessionsResult |\n`listSessions`}). See {@link PaginatedParams} for the full pagination\ncontract shared by every paginated command.", "properties": { - "nextCursor": { + "kind": { + "const": "daily" + }, + "time": { + "$ref": "#/$defs/AutomationLocalTime" + }, + "timeZone": { "type": "string", - "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." + "description": "IANA time-zone identifier." } - } + }, + "required": [ + "kind", + "time", + "timeZone" + ] }, - "Implementation": { + "AutomationWeeklySchedule": { "type": "object", - "description": "Identifies a protocol implementation — the software (and build) on one end\nof the connection, as distinct from the {@link AgentInfo | agent persona} it\nhosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the\nclient side and {@link InitializeResult.serverInfo | `serverInfo`} on the\nserver side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's\n`Implementation`.\n\nThis is **informational only**: it exists for logging, telemetry, an\nabout/status affordance, and — as a last resort — a known-issue workaround\nfor a specific buggy build. It is **not** a feature-detection mechanism.\nFeature availability stays with the capability model\n({@link ClientCapabilities} and the various `*.capabilities` declarations);\nimplementations SHOULD NOT gate protocol behaviour on parsing\n{@link Implementation.version | `version`}.", "properties": { - "name": { - "type": "string", - "description": "Implementation name, e.g. a product or package identifier." + "kind": { + "const": "weekly" }, - "version": { - "type": "string", - "description": "Implementation version. A [SemVer](https://semver.org) string is\nrecommended but not required." + "weekday": { + "$ref": "#/$defs/AutomationWeekday" }, - "title": { + "time": { + "$ref": "#/$defs/AutomationLocalTime" + }, + "timeZone": { "type": "string", - "description": "Optional human-readable display name." + "description": "IANA time-zone identifier." } }, "required": [ - "name" + "kind", + "weekday", + "time", + "timeZone" ] }, - "InitializeParams": { + "AutomationCronSchedule": { "type": "object", - "description": "Establishes a new connection and negotiates the protocol version.\nThis MUST be the first message sent by the client.", "properties": { - "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." - }, - "protocolVersions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Protocol versions the client is willing to speak, ordered from most\npreferred to least preferred. Each entry is a [SemVer](https://semver.org)\n`MAJOR.MINOR.PATCH` string (e.g. `\"0.1.0\"`).\n\nThe server selects one entry and returns it as `InitializeResult.protocolVersion`.\nIf the server cannot speak any of the offered versions, it MUST return\nerror code `-32005` (`UnsupportedProtocolVersion`)." + "kind": { + "const": "cron" }, - "clientId": { + "expression": { "type": "string", - "description": "Unique client identifier" + "description": "Standard five-field Unix cron expression." }, - "clientInfo": { - "$ref": "#/$defs/Implementation", - "description": "Optional identity of the client implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Distinct from {@link InitializeParams.clientId | `clientId`},\nwhich is an opaque per-connection identifier used for reconnection, not a\nhuman-readable implementation name." - }, - "initialSubscriptions": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "URIs to subscribe to during handshake" - }, - "locale": { + "timeZone": { "type": "string", - "description": "IETF BCP 47 language tag indicating the client's preferred locale\n(e.g. `\"en-US\"`, `\"ja\"`). The server SHOULD use this to localise\nuser-facing strings such as confirmation option labels." - }, - "capabilities": { - "$ref": "#/$defs/ClientCapabilities", - "description": "Optional client capability declarations.\n\nServers SHOULD only advertise features whose corresponding client\ncapability is set here. Absent means \"not declared\" — the server\nMUST assume the client does not support the feature." + "description": "IANA time-zone identifier." } }, "required": [ - "channel", - "protocolVersions", - "clientId" + "kind", + "expression", + "timeZone" ] }, - "ClientCapabilities": { + "AutomationScheduleTrigger": { "type": "object", - "description": "Optional capabilities a client declares during `initialize`.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities\nare reserved for future per-capability options.", "properties": { - "mcpApps": { - "type": "object", - "additionalProperties": {}, - "description": "Client can render\n[MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.\nit can host the View sandbox, run the `ui/*` protocol against it,\nand forward `mcp://`-channel traffic on the App's behalf.\n\nHosts SHOULD only populate\n{@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`}\n(and expose the corresponding\n{@link McpServerCustomization.channel | `mcp://` channel}) when this\ncapability is declared. Clients that omit it MUST treat\nApp-bearing tool calls as ordinary MCP tool calls." + "id": { + "type": "string", + "description": "Stable within the automation definition." + }, + "kind": { + "const": "schedule" + }, + "schedule": { + "$ref": "#/$defs/AutomationSchedule" + }, + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy" } - } + }, + "required": [ + "id", + "kind", + "schedule" + ] }, - "InitializeResult": { + "AutomationEventTrigger": { "type": "object", - "description": "Result of the `initialize` command.\n\n`protocolVersion` is the version the server has selected from the client's\n`protocolVersions` list. The client and server MUST use this version for\nthe rest of the connection. If the server cannot speak any of the offered\nversions it MUST return error code `-32005` (`UnsupportedProtocolVersion`)\ninstead of a result.", "properties": { - "protocolVersion": { + "id": { "type": "string", - "description": "Protocol version selected by the server. MUST be one of the entries in\n`InitializeParams.protocolVersions`. Formatted as a [SemVer](https://semver.org)\n`MAJOR.MINOR.PATCH` string (e.g. `\"0.1.0\"`)." - }, - "serverSeq": { - "type": "number", - "description": "Current server sequence number" + "description": "Stable within the automation definition." }, - "serverInfo": { - "$ref": "#/$defs/Implementation", - "description": "Optional identity of the server implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`}\nidentifies the negotiated protocol, `serverInfo` identifies the host\nsoftware behind it." - }, - "snapshots": { - "type": "array", - "items": { - "$ref": "#/$defs/Snapshot" - }, - "description": "Snapshots for each `initialSubscriptions` URI" + "kind": { + "const": "event" }, - "defaultDirectory": { - "$ref": "#/$defs/URI", - "description": "Suggested default directory for remote filesystem browsing" + "type": { + "type": "string", + "description": "Stable host-defined trigger type." }, - "completionTriggerCharacters": { + "events": { "type": "array", "items": { "type": "string" }, - "description": "Characters that, when typed in a {@link Message} input, SHOULD cause\nthe client to issue a `completions` request with\n{@link CompletionItemKind.UserMessage}. Typically includes characters like\n`'@'` or `'/'`." - }, - "terminalCommandPrefix": { - "type": "string", - "description": "Prefix that the host recognizes at the start of a user {@link Message.text}\nas a shorthand for executing the remainder as a terminal command. Currently\nthe standardized convention is `\"!\"`; absence means the host does not\nsupport command prefixes." + "description": "Selected event actions." }, - "telemetry": { - "$ref": "#/$defs/TelemetryCapabilities", - "description": "OTLP telemetry channels the host emits, if any. Each populated field is\neither a literal `ahp-otlp:` channel URI or an RFC 6570 URI template a\nclient expands before subscribing (currently only the `logs` channel\ndefines a template variable, `{level}`, for subscriber-side severity\nfiltering). Clients MAY ignore signals they cannot process." + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Schema-defined values. Unknown entries must survive round-trips." } }, "required": [ - "protocolVersion", - "serverSeq", - "snapshots" + "id", + "kind", + "type", + "events" ] }, - "PingParams": { + "AutomationTriggerEventDefinition": { "type": "object", - "description": "Verifies that the AHP connection is still alive and keeps it from being\nclosed by idle-timeout intermediaries (proxies, load balancers, etc.).\n\nThe server MUST respond regardless of whether the client has completed\n`initialize` or holds any subscriptions. Ping carries no payload in either\ndirection; the response itself is the signal.", "properties": { - "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "id": { + "type": "string" }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + "title": { + "type": "string" + }, + "description": { + "type": "string" } }, "required": [ - "channel" + "id", + "title" ] }, - "ReconnectParams": { + "AutomationTriggerDefinition": { "type": "object", - "description": "Re-establishes a dropped connection. The server replays missed actions or\nprovides fresh snapshots.", "properties": { - "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + "type": { + "type": "string" }, - "clientId": { - "type": "string", - "description": "Client identifier from the original connection" + "title": { + "type": "string" }, - "lastSeenServerSeq": { - "type": "number", - "description": "Last `serverSeq` the client received" + "description": { + "type": "string" }, - "subscriptions": { + "events": { "type": "array", "items": { - "$ref": "#/$defs/URI" - }, - "description": "URIs the client was subscribed to" + "$ref": "#/$defs/AutomationTriggerEventDefinition" + } + }, + "configSchema": { + "$ref": "#/$defs/ConfigSchema" } }, "required": [ - "channel", - "clientId", - "lastSeenServerSeq", - "subscriptions" + "type", + "title", + "events" ] }, - "ReconnectReplayResult": { + "AutomationSessionTemplate": { "type": "object", - "description": "Reconnect result when the server can replay from the requested sequence.\n\nThe server MUST include all replayed data in the response.", "properties": { - "type": { - "const": "replay", - "description": "Discriminant" - }, - "actions": { - "type": "array", - "items": { - "$ref": "#/$defs/ActionEnvelope" - }, - "description": "Missed action envelopes since `lastSeenServerSeq`" + "provider": { + "type": "string" }, - "missing": { + "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "URIs from `ReconnectParams.subscriptions` that the server cannot resume.\nThis includes resources that no longer exist (e.g. disposed sessions or\nterminals) as well as resources the client is no longer permitted to\nobserve. Clients SHOULD drop these from their local subscription set." - } - }, - "required": [ + "description": "Absence means a workspace-less session." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values resolved through `resolveSessionConfig`." + } + } + }, + "AutomationDefinition": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "Initial user message sent to each new session." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate" + }, + "enabled": { + "type": "boolean", + "description": "Controls automatic triggers; manual runs remain permitted." + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + }, + "description": "Empty means manual-only." + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "title", + "message", + "session", + "enabled", + "triggers" + ] + }, + "AutomationRuntimeState": { + "type": "object", + "properties": { + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + } + }, + "AutomationSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "title": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "triggerCount": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "lastRun": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "revision": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "revision": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries." + }, + "runsNextCursor": { + "type": "string" + }, + "runtime": { + "$ref": "#/$defs/AutomationRuntimeState" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationRunBlocker": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind" + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "trigger" + }, + "triggerId": { + "type": "string" + }, + "scheduledFor": { + "type": "string" + }, + "catchUp": { + "type": "boolean" + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined event provenance containing no secrets." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "usage": { + "$ref": "#/$defs/UsageInfo" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "AutomationFailedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "failed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "error": { + "$ref": "#/$defs/ErrorInfo" + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "AutomationCancelledRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "cancelled" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "AutomationRunArtifact": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "AutomationRunSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "sessionCount": { + "type": "number" + }, + "artifactCount": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "AutomationRunState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "sessions": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "artifacts": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessions", + "artifacts", + "operations" + ] + }, + "BaseParams": { + "type": "object", + "description": "Base shape every command's params extends.\n\n`channel` identifies the channel the command targets, mirroring the\n`channel` field on every protocol notification. For commands that operate\non a specific channel (a session, terminal, or changeset), `channel` is\nthat channel's URI. For commands that are connection-level rather than\nchannel-scoped (e.g. {@link InitializeParams | `initialize`},\n{@link PingParams | `ping`}, {@link ListSessionsParams | `listSessions`},\nthe `resource*` filesystem commands, and {@link AuthenticateParams |\n`authenticate`}), the params type narrows `channel` to the literal\nroot URI `'ahp-root://'`.\n\nThis invariant lets implementations route every incoming message —\nrequest, response, or notification — by inspecting `params.channel`\nwithout needing to know the per-method param shape.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + } + }, + "required": [ + "channel" + ] + }, + "PaginatedParams": { + "type": "object", + "description": "Cursor-based pagination inputs, mixed into the params of any list command\nthat can page a large result set (e.g. {@link ListSessionsParams |\n`listSessions`}). The paired output is {@link PaginatedResult}.\n\nPagination is **opaque and cursor-based**, mirroring the shape `fetchTurns`\nalready uses for chat history: the server owns the ordering and keyset, and\nthe client walks pages by echoing the cursor from the previous\n{@link PaginatedResult.nextCursor} back on the next request.\n\nThe contract every paginated command shares:\n\n- To fetch the first page, omit `cursor`. Supply `limit` to bound the page.\n- If the result carries a {@link PaginatedResult.nextCursor}, more entries\n exist — pass it back as `cursor` to fetch the following page. A missing\n `nextCursor` signals the end of the collection.\n- Cursors are **server-defined and opaque**: clients MUST NOT parse, modify,\n or persist them across connections. An unrecognised cursor SHOULD be\n rejected with an `InvalidParams` error.\n- Pagination is **fully additive**: a client that omits `limit`/`cursor` and\n ignores `nextCursor` sees the pre-pagination behaviour (subject to any\n server-imposed cap), and a server that does not paginate ignores the inputs\n and returns everything in a single page.", + "properties": { + "limit": { + "type": "number", + "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." + }, + "cursor": { + "type": "string", + "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + } + } + }, + "PaginatedResult": { + "type": "object", + "description": "Cursor-based pagination output, extended by the result of any list command\nthat can page a large result set (e.g. {@link ListSessionsResult |\n`listSessions`}). See {@link PaginatedParams} for the full pagination\ncontract shared by every paginated command.", + "properties": { + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." + } + } + }, + "Implementation": { + "type": "object", + "description": "Identifies a protocol implementation — the software (and build) on one end\nof the connection, as distinct from the {@link AgentInfo | agent persona} it\nhosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the\nclient side and {@link InitializeResult.serverInfo | `serverInfo`} on the\nserver side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's\n`Implementation`.\n\nThis is **informational only**: it exists for logging, telemetry, an\nabout/status affordance, and — as a last resort — a known-issue workaround\nfor a specific buggy build. It is **not** a feature-detection mechanism.\nFeature availability stays with the capability model\n({@link ClientCapabilities} and the various `*.capabilities` declarations);\nimplementations SHOULD NOT gate protocol behaviour on parsing\n{@link Implementation.version | `version`}.", + "properties": { + "name": { + "type": "string", + "description": "Implementation name, e.g. a product or package identifier." + }, + "version": { + "type": "string", + "description": "Implementation version. A [SemVer](https://semver.org) string is\nrecommended but not required." + }, + "title": { + "type": "string", + "description": "Optional human-readable display name." + } + }, + "required": [ + "name" + ] + }, + "InitializeParams": { + "type": "object", + "description": "Establishes a new connection and negotiates the protocol version.\nThis MUST be the first message sent by the client.", + "properties": { + "channel": { + "type": "string", + "enum": [ + "ahp-root://" + ] + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, + "protocolVersions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Protocol versions the client is willing to speak, ordered from most\npreferred to least preferred. Each entry is a [SemVer](https://semver.org)\n`MAJOR.MINOR.PATCH` string (e.g. `\"0.1.0\"`).\n\nThe server selects one entry and returns it as `InitializeResult.protocolVersion`.\nIf the server cannot speak any of the offered versions, it MUST return\nerror code `-32005` (`UnsupportedProtocolVersion`)." + }, + "clientId": { + "type": "string", + "description": "Unique client identifier" + }, + "clientInfo": { + "$ref": "#/$defs/Implementation", + "description": "Optional identity of the client implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Distinct from {@link InitializeParams.clientId | `clientId`},\nwhich is an opaque per-connection identifier used for reconnection, not a\nhuman-readable implementation name." + }, + "initialSubscriptions": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "URIs to subscribe to during handshake" + }, + "locale": { + "type": "string", + "description": "IETF BCP 47 language tag indicating the client's preferred locale\n(e.g. `\"en-US\"`, `\"ja\"`). The server SHOULD use this to localise\nuser-facing strings such as confirmation option labels." + }, + "capabilities": { + "$ref": "#/$defs/ClientCapabilities", + "description": "Optional client capability declarations.\n\nServers SHOULD only advertise features whose corresponding client\ncapability is set here. Absent means \"not declared\" — the server\nMUST assume the client does not support the feature." + } + }, + "required": [ + "channel", + "protocolVersions", + "clientId" + ] + }, + "ClientCapabilities": { + "type": "object", + "description": "Optional capabilities a client declares during `initialize`.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities\nare reserved for future per-capability options.", + "properties": { + "mcpApps": { + "type": "object", + "additionalProperties": {}, + "description": "Client can render\n[MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.\nit can host the View sandbox, run the `ui/*` protocol against it,\nand forward `mcp://`-channel traffic on the App's behalf.\n\nHosts SHOULD only populate\n{@link McpServerCustomization.mcpApp | `McpServerCustomization.mcpApp`}\n(and expose the corresponding\n{@link McpServerCustomization.channel | `mcp://` channel}) when this\ncapability is declared. Clients that omit it MUST treat\nApp-bearing tool calls as ordinary MCP tool calls." + } + } + }, + "InitializeResult": { + "type": "object", + "description": "Result of the `initialize` command.\n\n`protocolVersion` is the version the server has selected from the client's\n`protocolVersions` list. The client and server MUST use this version for\nthe rest of the connection. If the server cannot speak any of the offered\nversions it MUST return error code `-32005` (`UnsupportedProtocolVersion`)\ninstead of a result.", + "properties": { + "protocolVersion": { + "type": "string", + "description": "Protocol version selected by the server. MUST be one of the entries in\n`InitializeParams.protocolVersions`. Formatted as a [SemVer](https://semver.org)\n`MAJOR.MINOR.PATCH` string (e.g. `\"0.1.0\"`)." + }, + "serverSeq": { + "type": "number", + "description": "Current server sequence number" + }, + "serverInfo": { + "$ref": "#/$defs/Implementation", + "description": "Optional identity of the server implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`}\nidentifies the negotiated protocol, `serverInfo` identifies the host\nsoftware behind it." + }, + "snapshots": { + "type": "array", + "items": { + "$ref": "#/$defs/Snapshot" + }, + "description": "Snapshots for each `initialSubscriptions` URI" + }, + "defaultDirectory": { + "$ref": "#/$defs/URI", + "description": "Suggested default directory for remote filesystem browsing" + }, + "completionTriggerCharacters": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Characters that, when typed in a {@link Message} input, SHOULD cause\nthe client to issue a `completions` request with\n{@link CompletionItemKind.UserMessage}. Typically includes characters like\n`'@'` or `'/'`." + }, + "terminalCommandPrefix": { + "type": "string", + "description": "Prefix that the host recognizes at the start of a user {@link Message.text}\nas a shorthand for executing the remainder as a terminal command. Currently\nthe standardized convention is `\"!\"`; absence means the host does not\nsupport command prefixes." + }, + "telemetry": { + "$ref": "#/$defs/TelemetryCapabilities", + "description": "OTLP telemetry channels the host emits, if any. Each populated field is\neither a literal `ahp-otlp:` channel URI or an RFC 6570 URI template a\nclient expands before subscribing (currently only the `logs` channel\ndefines a template variable, `{level}`, for subscriber-side severity\nfiltering). Clients MAY ignore signals they cannot process." + }, + "automations": { + "$ref": "#/$defs/AutomationCapabilities", + "description": "Host automation support. Absence means unsupported." + } + }, + "required": [ + "protocolVersion", + "serverSeq", + "snapshots" + ] + }, + "AutomationCapabilities": { + "type": "object", + "properties": { + "execution": { + "$ref": "#/$defs/AutomationExecutionCapabilities" + }, + "create": { + "$ref": "#/$defs/AutomationCreateCapability" + }, + "schedules": { + "$ref": "#/$defs/AutomationScheduleCapabilities" + }, + "runCancellation": { + "$ref": "#/$defs/AutomationRunCancellationCapability" + }, + "schedulePreview": { + "$ref": "#/$defs/AutomationSchedulePreviewCapability" + }, + "runHistoryLimit": { + "type": "number" + } + }, + "required": [ + "execution" + ] + }, + "AutomationExecutionCapabilities": { + "type": "object", + "properties": { + "lifetime": { + "$ref": "#/$defs/AutomationExecutionLifetime" + } + }, + "required": [ + "lifetime" + ] + }, + "AutomationCreateCapability": { + "type": "object", + "properties": {} + }, + "AutomationScheduleCapabilities": { + "type": "object", + "properties": { + "kinds": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationScheduleKind" + } + }, + "cron": { + "$ref": "#/$defs/AutomationCronScheduleCapability" + } + }, + "required": [ + "kinds" + ] + }, + "AutomationCronScheduleCapability": { + "type": "object", + "properties": { + "dialect": { + "type": "string", + "enum": [ + "unix5" + ] + }, + "minIntervalMinutes": { + "type": "number" + } + }, + "required": [ + "dialect" + ] + }, + "AutomationRunCancellationCapability": { + "type": "object", + "properties": {} + }, + "AutomationSchedulePreviewCapability": { + "type": "object", + "properties": {} + }, + "PingParams": { + "type": "object", + "description": "Verifies that the AHP connection is still alive and keeps it from being\nclosed by idle-timeout intermediaries (proxies, load balancers, etc.).\n\nThe server MUST respond regardless of whether the client has completed\n`initialize` or holds any subscriptions. Ping carries no payload in either\ndirection; the response itself is the signal.", + "properties": { + "channel": { + "type": "string", + "enum": [ + "ahp-root://" + ] + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + } + }, + "required": [ + "channel" + ] + }, + "ReconnectParams": { + "type": "object", + "description": "Re-establishes a dropped connection. The server replays missed actions or\nprovides fresh snapshots.", + "properties": { + "channel": { + "type": "string", + "enum": [ + "ahp-root://" + ] + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, + "clientId": { + "type": "string", + "description": "Client identifier from the original connection" + }, + "lastSeenServerSeq": { + "type": "number", + "description": "Last `serverSeq` the client received" + }, + "subscriptions": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "URIs the client was subscribed to" + } + }, + "required": [ + "channel", + "clientId", + "lastSeenServerSeq", + "subscriptions" + ] + }, + "ReconnectReplayResult": { + "type": "object", + "description": "Reconnect result when the server can replay from the requested sequence.\n\nThe server MUST include all replayed data in the response.", + "properties": { + "type": { + "const": "replay", + "description": "Discriminant" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/$defs/ActionEnvelope" + }, + "description": "Missed action envelopes since `lastSeenServerSeq`" + }, + "missing": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "URIs from `ReconnectParams.subscriptions` that the server cannot resume.\nThis includes resources that no longer exist (e.g. disposed sessions or\nterminals) as well as resources the client is no longer permitted to\nobserve. Clients SHOULD drop these from their local subscription set." + } + }, + "required": [ "type", "actions", "missing" @@ -6438,38 +7245,295 @@ ], "description": "Glob patterns or paths relative to `uri` to exclude from reporting.\nWrapped in `{ items }` for forward compatibility." }, - "includes": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "items" - ], - "description": "Glob patterns or paths relative to `uri` to restrict reporting to.\nOmit to report every change under `uri` subject to `excludes`.\nWrapped in `{ items }` for forward compatibility." + "includes": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "items" + ], + "description": "Glob patterns or paths relative to `uri` to restrict reporting to.\nOmit to report every change under `uri` subject to `excludes`.\nWrapped in `{ items }` for forward compatibility." + } + }, + "required": [ + "channel", + "uri" + ] + }, + "CreateResourceWatchResult": { + "type": "object", + "description": "Result of the `createResourceWatch` command.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Receiver-assigned watch channel URI (`ahp-resource-watch:/`). The\ncaller subscribes to this URI to start receiving change events and\nunsubscribes to release the watcher." + } + }, + "required": [ + "channel" + ] + }, + "ListAutomationsParams": { + "type": "object", + "properties": { + "channel": { + "type": "string", + "enum": [ + "ahp-root://" + ] + }, + "limit": { + "type": "number", + "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." + }, + "cursor": { + "type": "string", + "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "channel" + ] + }, + "ListAutomationsResult": { + "type": "object", + "properties": { + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page. Present when more entries exist beyond the\nreturned page; absent signals the end of the collection. Pass it back as\n{@link PaginatedParams.cursor} to fetch the following page." + }, + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationSummary" + } + } + }, + "required": [ + "items" + ] + }, + "ListAutomationTriggerDefinitionsParams": { + "type": "object", + "properties": { + "channel": { + "type": "string", + "enum": [ + "ahp-root://" + ] + }, + "provider": { + "type": "string" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "sessionConfig": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "channel" + ] + }, + "ListAutomationTriggerDefinitionsResult": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTriggerDefinition" + } + } + }, + "required": [ + "items" + ] + }, + "CreateAutomationParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "import": { + "type": "object", + "properties": { + "source": { + "type": "string" + }, + "batchId": { + "type": "string" + }, + "itemId": { + "type": "string" + } + }, + "required": [ + "source", + "batchId", + "itemId" + ] + } + }, + "required": [ + "channel", + "definition" + ] + }, + "AutomationDefinitionPatch": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "$ref": "#/$defs/Message" + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate" + }, + "enabled": { + "type": "boolean" + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + } + }, + "UpdateAutomationParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI" + }, + "expectedRevision": { + "type": "number" + }, + "changes": { + "$ref": "#/$defs/AutomationDefinitionPatch" + } + }, + "required": [ + "channel", + "expectedRevision", + "changes" + ] + }, + "DisposeAutomationParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + } + }, + "required": [ + "channel" + ] + }, + "RunAutomationParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "channel", + "requestId" + ] + }, + "RunAutomationResult": { + "type": "object", + "properties": { + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "run" + ] + }, + "FetchAutomationRunsParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." + }, + "cursor": { + "type": "string" + } + }, + "required": [ + "channel" + ] + }, + "FetchAutomationRunsResult": { + "type": "object", + "properties": {} + }, + "PreviewAutomationScheduleParams": { + "type": "object", + "properties": { + "channel": { + "type": "string", + "enum": [ + "ahp-root://" + ] + }, + "schedule": { + "$ref": "#/$defs/AutomationSchedule" + }, + "count": { + "type": "number" } }, "required": [ "channel", - "uri" + "schedule" ] }, - "CreateResourceWatchResult": { + "PreviewAutomationScheduleResult": { "type": "object", - "description": "Result of the `createResourceWatch` command.", "properties": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Receiver-assigned watch channel URI (`ahp-resource-watch:/`). The\ncaller subscribes to this URI to start receiving change events and\nunsubscribes to release the watcher." + "items": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "channel" + "items" ] }, "URI": { @@ -6528,6 +7592,9 @@ "type": "number", "description": "Bitset of summary-level session status flags.\n\nUse bitwise checks instead of equality for non-terminal activity. For example,\n`status & SessionStatus.InProgress` matches both ordinary in-progress turns\nand turns that are paused waiting for input." }, + "SessionOrigin": { + "$ref": "#/$defs/AutomationSessionOrigin" + }, "SessionLifecycle": { "enum": [ "creating", @@ -7053,6 +8120,122 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, + "AutomationWeekday": { + "enum": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "type": "string" + }, + "AutomationSchedule": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationHourlySchedule" + }, + { + "$ref": "#/$defs/AutomationDailySchedule" + }, + { + "$ref": "#/$defs/AutomationWeeklySchedule" + }, + { + "$ref": "#/$defs/AutomationCronSchedule" + } + ] + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string" + }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$ref": "#/$defs/AutomationEventTrigger" + } + ] + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "type": "string" + }, + "AutomationRunBlockerKind": { + "enum": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "type": "string" + }, + "AutomationRunCause": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ] + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ] + }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string" + }, + "AutomationExecutionLifetime": { + "enum": [ + "hostLifetime", + "managed" + ], + "type": "string" + }, + "AutomationScheduleKind": { + "enum": [ + "hourly", + "daily", + "weekly", + "cron" + ], + "type": "string" + }, "ActionEnvelope": { "type": "object", "description": "Every action is wrapped in an `ActionEnvelope`.\n\nThe envelope identifies the channel the action belongs to (e.g.\n`ahp-root://` for root actions, the session URI for session actions, the\nterminal URI for terminal actions). Individual action payloads carry only\nfields that are intrinsic to the action; the channel comes from the\nenvelope so that any subscribable resource can route its actions uniformly.", @@ -7336,6 +8519,39 @@ }, { "$ref": "#/$defs/ResourceWatchChangedAction" + }, + { + "$ref": "#/$defs/AutomationDefinitionChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunSummarySetAction" + }, + { + "$ref": "#/$defs/AutomationRunSummaryRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunsLoadedAction" + }, + { + "$ref": "#/$defs/AutomationRunLifecycleChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunSessionSetAction" + }, + { + "$ref": "#/$defs/AutomationRunSessionRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunPrimarySessionChangedAction" + }, + { + "$ref": "#/$defs/AutomationRunArtifactSetAction" + }, + { + "$ref": "#/$defs/AutomationRunArtifactRemovedAction" + }, + { + "$ref": "#/$defs/AutomationRunCancelRequestedAction" } ], "description": "Discriminated union of all state actions." @@ -9381,6 +10597,190 @@ "changes" ] }, + "AutomationDefinitionChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/definitionChanged" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "revision": { + "type": "number" + }, + "modifiedAt": { + "type": "string" + }, + "nextRunAt": { + "type": "string" + } + }, + "required": [ + "type", + "definition", + "revision", + "modifiedAt" + ] + }, + "AutomationRunSummarySetAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runSummarySet" + }, + "run": { + "$ref": "#/$defs/AutomationRunSummary" + } + }, + "required": [ + "type", + "run" + ] + }, + "AutomationRunSummaryRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runSummaryRemoved" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "run" + ] + }, + "AutomationRunsLoadedAction": { + "type": "object", + "properties": { + "type": { + "const": "automation/runsLoaded" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + } + }, + "nextCursor": { + "type": "string" + } + }, + "required": [ + "type", + "runs" + ] + }, + "AutomationRunLifecycleChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/lifecycleChanged" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + } + }, + "required": [ + "type", + "lifecycle", + "operations" + ] + }, + "AutomationRunSessionSetAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/sessionSet" + }, + "session": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "session" + ] + }, + "AutomationRunSessionRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/sessionRemoved" + }, + "session": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type", + "session" + ] + }, + "AutomationRunPrimarySessionChangedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/primarySessionChanged" + }, + "primarySession": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "type" + ] + }, + "AutomationRunArtifactSetAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/artifactSet" + }, + "artifact": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "required": [ + "type", + "artifact" + ] + }, + "AutomationRunArtifactRemovedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/artifactRemoved" + }, + "artifactId": { + "type": "string" + } + }, + "required": [ + "type", + "artifactId" + ] + }, + "AutomationRunCancelRequestedAction": { + "type": "object", + "properties": { + "type": { + "const": "automationRun/cancelRequested" + } + }, + "required": [ + "type" + ] + }, "ChatToolCallApprovedAction": { "type": "object", "description": "Client approves a pending tool call. The tool transitions to `running`.", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index ab95e4b4c..b05836290 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -94,6 +94,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -140,6 +144,51 @@ "changes" ] }, + "AutomationAddedParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI" + }, + "summary": { + "$ref": "#/$defs/AutomationSummary" + } + }, + "required": [ + "channel", + "summary" + ] + }, + "AutomationRemovedParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "channel", + "automation" + ] + }, + "AutomationSummaryChangedParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI" + }, + "summary": { + "$ref": "#/$defs/AutomationSummary" + } + }, + "required": [ + "channel", + "summary" + ] + }, "ProgressParams": { "type": "object", "description": "Generic progress notification for a long-running operation.\n\nA client opts in to progress for a request by including a `progressToken` in\nthat request (today: the `progressToken` field on `createSession`). If the\nserver does long-running work to service the request — e.g. lazily\ndownloading an agent's native SDK the first time a session of that provider\nis materialized — it emits `progress` notifications carrying the same token.\n\nThe notification is operation-agnostic: it says nothing about *what* is\nprogressing. The client correlates `progressToken` back to the request it\noriginated from (and thus the UI surface awaiting it) and renders its own\nlocalized indicator. The same channel serves any future long-running\noperation without a new method.\n\nSemantics:\n\n- `progress` is monotonically non-decreasing for a given `progressToken`.\n- `total` is present only when the server knows the magnitude up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- The operation is complete when `progress === total`. The server MUST emit a\n final frame satisfying `progress === total`; when the total was never\n known, it sets `total` to the final `progress` on that frame. No further\n frames reference the token afterwards.\n- The server MAY emit no progress at all (e.g. the work was already done);\n the client then never shows an indicator.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives the terminal frame SHOULD expire\n the indicator after an idle timeout.", @@ -243,6 +292,15 @@ { "$ref": "#/$defs/SessionSummaryChangedParams" }, + { + "$ref": "#/$defs/AutomationAddedParams" + }, + { + "$ref": "#/$defs/AutomationRemovedParams" + }, + { + "$ref": "#/$defs/AutomationSummaryChangedParams" + }, { "$ref": "#/$defs/ProgressParams" }, @@ -670,6 +728,12 @@ }, { "$ref": "#/$defs/ChatState" + }, + { + "$ref": "#/$defs/AutomationState" + }, + { + "$ref": "#/$defs/AutomationRunState" } ], "description": "The current state of the resource" @@ -897,6 +961,25 @@ "values" ] }, + "AutomationSessionOrigin": { + "type": "object", + "properties": { + "kind": { + "const": "automation" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "kind", + "automation", + "run" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -917,6 +1000,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -959,6 +1046,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -1263,6 +1354,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -5142,165 +5237,851 @@ "type" ] }, - "URI": { - "type": "string", - "description": "A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)." - }, - "AuthRequiredReason": { - "enum": [ - "required", - "expired" - ], - "type": "string", - "description": "Reason why authentication is required." - }, - "SessionStatus": { - "enum": [ - 1, - 2, - 8, - 24, - 32, - 64 - ], - "type": "number", - "description": "Bitset of summary-level session status flags.\n\nUse bitwise checks instead of equality for non-terminal activity. For example,\n`status & SessionStatus.InProgress` matches both ordinary in-progress turns\nand turns that are paused waiting for input." - }, - "JsonPrimitive": { - "oneOf": [ - { - "type": "string" - }, - { + "AutomationLocalTime": { + "type": "object", + "properties": { + "hour": { "type": "number" }, - { - "type": "boolean" - }, - { - "type": "null" + "minute": { + "type": "number" } - ], - "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "required": [ + "hour", + "minute" + ] }, - "Customization": { - "oneOf": [ - { - "$ref": "#/$defs/PluginCustomization" - }, - { - "$ref": "#/$defs/DirectoryCustomization" - }, - { - "$ref": "#/$defs/McpServerCustomization" + "AutomationHourlySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "hourly" } - ], - "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." - }, - "PolicyState": { - "enum": [ - "enabled", - "disabled", - "unconfigured" - ], - "type": "string", - "description": "Policy configuration state for a model." - }, - "SessionLifecycle": { - "enum": [ - "creating", - "ready", - "creationFailed" - ], - "type": "string", - "description": "Session initialization state." + }, + "required": [ + "kind" + ] }, - "SessionInputRequest": { - "oneOf": [ - { - "$ref": "#/$defs/SessionChatInputRequest" - }, - { - "$ref": "#/$defs/SessionToolConfirmationRequest" - }, - { - "$ref": "#/$defs/SessionToolClientExecutionRequest" + "AutomationDailySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "daily" }, - { - "$ref": "#/$defs/SessionToolAuthenticationRequest" - } - ], - "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." - }, - "ToolCallConfirmationState": { - "oneOf": [ - { - "$ref": "#/$defs/ToolCallPendingConfirmationState" + "time": { + "$ref": "#/$defs/AutomationLocalTime" }, - { - "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." } - ], - "description": "The two tool-call states that block on a client confirmation: parameter\nconfirmation before execution ({@link ToolCallPendingConfirmationState}) and\nresult confirmation after execution\n({@link ToolCallPendingResultConfirmationState}).\n\n{@link ToolCallAuthRequiredState} is intentionally **not** part of this\nunion: it doesn't block on a `chat/toolCallConfirmed`-style client\ndecision, it blocks on the client completing an OAuth flow and calling\n`authenticate`. See {@link SessionToolAuthenticationRequest} for its\nsession-level surfacing.\n\nSurfaced at the session level by {@link SessionToolConfirmationRequest}." + }, + "required": [ + "kind", + "time", + "timeZone" + ] }, - "ToolCallState": { - "oneOf": [ - { - "$ref": "#/$defs/ToolCallStreamingState" - }, - { - "$ref": "#/$defs/ToolCallPendingConfirmationState" + "AutomationWeeklySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "weekly" }, - { - "$ref": "#/$defs/ToolCallRunningState" + "weekday": { + "$ref": "#/$defs/AutomationWeekday" }, - { - "$ref": "#/$defs/ToolCallAuthRequiredState" + "time": { + "$ref": "#/$defs/AutomationLocalTime" }, - { - "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." + } + }, + "required": [ + "kind", + "weekday", + "time", + "timeZone" + ] + }, + "AutomationCronSchedule": { + "type": "object", + "properties": { + "kind": { + "const": "cron" }, - { - "$ref": "#/$defs/ToolCallCompletedState" + "expression": { + "type": "string", + "description": "Standard five-field Unix cron expression." }, - { - "$ref": "#/$defs/ToolCallCancelledState" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." } - ], - "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." + }, + "required": [ + "kind", + "expression", + "timeZone" + ] }, - "CustomizationLoadState": { - "oneOf": [ - { - "$ref": "#/$defs/CustomizationLoadingState" + "AutomationScheduleTrigger": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable within the automation definition." }, - { - "$ref": "#/$defs/CustomizationLoadedState" + "kind": { + "const": "schedule" }, - { - "$ref": "#/$defs/CustomizationDegradedState" + "schedule": { + "$ref": "#/$defs/AutomationSchedule" }, - { - "$ref": "#/$defs/CustomizationErrorState" + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy" } - ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "required": [ + "id", + "kind", + "schedule" + ] }, - "ChildCustomization": { - "oneOf": [ - { - "$ref": "#/$defs/AgentCustomization" + "AutomationEventTrigger": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable within the automation definition." }, - { - "$ref": "#/$defs/SkillCustomization" + "kind": { + "const": "event" }, - { - "$ref": "#/$defs/PromptCustomization" + "type": { + "type": "string", + "description": "Stable host-defined trigger type." }, - { - "$ref": "#/$defs/RuleCustomization" + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selected event actions." }, - { + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Schema-defined values. Unknown entries must survive round-trips." + } + }, + "required": [ + "id", + "kind", + "type", + "events" + ] + }, + "AutomationTriggerEventDefinition": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "title" + ] + }, + "AutomationTriggerDefinition": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTriggerEventDefinition" + } + }, + "configSchema": { + "$ref": "#/$defs/ConfigSchema" + } + }, + "required": [ + "type", + "title", + "events" + ] + }, + "AutomationSessionTemplate": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Absence means a workspace-less session." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values resolved through `resolveSessionConfig`." + } + } + }, + "AutomationDefinition": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "Initial user message sent to each new session." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate" + }, + "enabled": { + "type": "boolean", + "description": "Controls automatic triggers; manual runs remain permitted." + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + }, + "description": "Empty means manual-only." + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "title", + "message", + "session", + "enabled", + "triggers" + ] + }, + "AutomationRuntimeState": { + "type": "object", + "properties": { + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + } + }, + "AutomationSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "title": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "triggerCount": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "lastRun": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "revision": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "revision": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries." + }, + "runsNextCursor": { + "type": "string" + }, + "runtime": { + "$ref": "#/$defs/AutomationRuntimeState" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationRunBlocker": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind" + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "trigger" + }, + "triggerId": { + "type": "string" + }, + "scheduledFor": { + "type": "string" + }, + "catchUp": { + "type": "boolean" + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined event provenance containing no secrets." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "usage": { + "$ref": "#/$defs/UsageInfo" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "AutomationFailedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "failed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "error": { + "$ref": "#/$defs/ErrorInfo" + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "AutomationCancelledRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "cancelled" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "AutomationRunArtifact": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "AutomationRunSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "sessionCount": { + "type": "number" + }, + "artifactCount": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "AutomationRunState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "sessions": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "artifacts": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessions", + "artifacts", + "operations" + ] + }, + "URI": { + "type": "string", + "description": "A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)." + }, + "AuthRequiredReason": { + "enum": [ + "required", + "expired" + ], + "type": "string", + "description": "Reason why authentication is required." + }, + "SessionStatus": { + "enum": [ + 1, + 2, + 8, + 24, + 32, + 64 + ], + "type": "number", + "description": "Bitset of summary-level session status flags.\n\nUse bitwise checks instead of equality for non-terminal activity. For example,\n`status & SessionStatus.InProgress` matches both ordinary in-progress turns\nand turns that are paused waiting for input." + }, + "SessionOrigin": { + "$ref": "#/$defs/AutomationSessionOrigin" + }, + "JsonPrimitive": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "Customization": { + "oneOf": [ + { + "$ref": "#/$defs/PluginCustomization" + }, + { + "$ref": "#/$defs/DirectoryCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." + }, + "PolicyState": { + "enum": [ + "enabled", + "disabled", + "unconfigured" + ], + "type": "string", + "description": "Policy configuration state for a model." + }, + "SessionLifecycle": { + "enum": [ + "creating", + "ready", + "creationFailed" + ], + "type": "string", + "description": "Session initialization state." + }, + "SessionInputRequest": { + "oneOf": [ + { + "$ref": "#/$defs/SessionChatInputRequest" + }, + { + "$ref": "#/$defs/SessionToolConfirmationRequest" + }, + { + "$ref": "#/$defs/SessionToolClientExecutionRequest" + }, + { + "$ref": "#/$defs/SessionToolAuthenticationRequest" + } + ], + "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." + }, + "ToolCallConfirmationState": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallPendingConfirmationState" + }, + { + "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + } + ], + "description": "The two tool-call states that block on a client confirmation: parameter\nconfirmation before execution ({@link ToolCallPendingConfirmationState}) and\nresult confirmation after execution\n({@link ToolCallPendingResultConfirmationState}).\n\n{@link ToolCallAuthRequiredState} is intentionally **not** part of this\nunion: it doesn't block on a `chat/toolCallConfirmed`-style client\ndecision, it blocks on the client completing an OAuth flow and calling\n`authenticate`. See {@link SessionToolAuthenticationRequest} for its\nsession-level surfacing.\n\nSurfaced at the session level by {@link SessionToolConfirmationRequest}." + }, + "ToolCallState": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallStreamingState" + }, + { + "$ref": "#/$defs/ToolCallPendingConfirmationState" + }, + { + "$ref": "#/$defs/ToolCallRunningState" + }, + { + "$ref": "#/$defs/ToolCallAuthRequiredState" + }, + { + "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + }, + { + "$ref": "#/$defs/ToolCallCompletedState" + }, + { + "$ref": "#/$defs/ToolCallCancelledState" + } + ], + "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." + }, + "CustomizationLoadState": { + "oneOf": [ + { + "$ref": "#/$defs/CustomizationLoadingState" + }, + { + "$ref": "#/$defs/CustomizationLoadedState" + }, + { + "$ref": "#/$defs/CustomizationDegradedState" + }, + { + "$ref": "#/$defs/CustomizationErrorState" + } + ], + "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "ChildCustomization": { + "oneOf": [ + { + "$ref": "#/$defs/AgentCustomization" + }, + { + "$ref": "#/$defs/SkillCustomization" + }, + { + "$ref": "#/$defs/PromptCustomization" + }, + { + "$ref": "#/$defs/RuleCustomization" + }, + { "$ref": "#/$defs/HookCustomization" }, { @@ -5730,6 +6511,106 @@ ], "type": "string", "description": "Discriminant for {@link ResourceChange.type}." + }, + "AutomationWeekday": { + "enum": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "type": "string" + }, + "AutomationSchedule": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationHourlySchedule" + }, + { + "$ref": "#/$defs/AutomationDailySchedule" + }, + { + "$ref": "#/$defs/AutomationWeeklySchedule" + }, + { + "$ref": "#/$defs/AutomationCronSchedule" + } + ] + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string" + }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$ref": "#/$defs/AutomationEventTrigger" + } + ] + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "type": "string" + }, + "AutomationRunBlockerKind": { + "enum": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "type": "string" + }, + "AutomationRunCause": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ] + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ] + }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string" } } } diff --git a/schema/state.schema.json b/schema/state.schema.json index d594f6b42..0734f52f8 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -418,6 +418,12 @@ }, { "$ref": "#/$defs/ChatState" + }, + { + "$ref": "#/$defs/AutomationState" + }, + { + "$ref": "#/$defs/AutomationRunState" } ], "description": "The current state of the resource" @@ -645,6 +651,25 @@ "values" ] }, + "AutomationSessionOrigin": { + "type": "object", + "properties": { + "kind": { + "const": "automation" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "run": { + "$ref": "#/$defs/URI" + } + }, + "required": [ + "kind", + "automation", + "run" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -665,6 +690,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -707,6 +736,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -1011,6 +1044,10 @@ "type": "string", "description": "Human-readable description of what the session is currently doing" }, + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." + }, "project": { "$ref": "#/$defs/ProjectInfo", "description": "Server-owned project for this session" @@ -4890,170 +4927,856 @@ "type" ] }, - "StringOrMarkdown": { - "oneOf": [ - { - "type": "string" + "AutomationLocalTime": { + "type": "object", + "properties": { + "hour": { + "type": "number" }, - { - "type": "object", - "properties": { - "markdown": { - "type": "string" - } - }, - "required": [ - "markdown" - ] + "minute": { + "type": "number" } - ], - "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." + }, + "required": [ + "hour", + "minute" + ] }, - "JsonPrimitive": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "null" + "AutomationHourlySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "hourly" } - ], - "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "required": [ + "kind" + ] }, - "SessionInputRequest": { - "oneOf": [ - { - "$ref": "#/$defs/SessionChatInputRequest" - }, - { - "$ref": "#/$defs/SessionToolConfirmationRequest" + "AutomationDailySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "daily" }, - { - "$ref": "#/$defs/SessionToolClientExecutionRequest" + "time": { + "$ref": "#/$defs/AutomationLocalTime" }, - { - "$ref": "#/$defs/SessionToolAuthenticationRequest" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." } - ], - "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." + }, + "required": [ + "kind", + "time", + "timeZone" + ] }, - "ChildCustomizationType": { - "oneOf": [ - { - "const": "agent" + "AutomationWeeklySchedule": { + "type": "object", + "properties": { + "kind": { + "const": "weekly" }, - { - "const": "skill" + "weekday": { + "$ref": "#/$defs/AutomationWeekday" }, - { - "const": "prompt" + "time": { + "$ref": "#/$defs/AutomationLocalTime" }, - { - "const": "rule" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." + } + }, + "required": [ + "kind", + "weekday", + "time", + "timeZone" + ] + }, + "AutomationCronSchedule": { + "type": "object", + "properties": { + "kind": { + "const": "cron" }, - { - "const": "hook" + "expression": { + "type": "string", + "description": "Standard five-field Unix cron expression." }, - { - "const": "mcpServer" + "timeZone": { + "type": "string", + "description": "IANA time-zone identifier." } - ], - "description": "Customization types that appear as children of a\n{@link PluginCustomization} or {@link DirectoryCustomization}." + }, + "required": [ + "kind", + "expression", + "timeZone" + ] }, - "CustomizationLoadState": { - "oneOf": [ - { - "$ref": "#/$defs/CustomizationLoadingState" + "AutomationScheduleTrigger": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable within the automation definition." }, - { - "$ref": "#/$defs/CustomizationLoadedState" + "kind": { + "const": "schedule" }, - { - "$ref": "#/$defs/CustomizationDegradedState" + "schedule": { + "$ref": "#/$defs/AutomationSchedule" }, - { - "$ref": "#/$defs/CustomizationErrorState" + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy" } - ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "required": [ + "id", + "kind", + "schedule" + ] }, - "ChildCustomization": { - "oneOf": [ - { - "$ref": "#/$defs/AgentCustomization" - }, - { - "$ref": "#/$defs/SkillCustomization" + "AutomationEventTrigger": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable within the automation definition." }, - { - "$ref": "#/$defs/PromptCustomization" + "kind": { + "const": "event" }, - { - "$ref": "#/$defs/RuleCustomization" + "type": { + "type": "string", + "description": "Stable host-defined trigger type." }, - { - "$ref": "#/$defs/HookCustomization" + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selected event actions." }, - { - "$ref": "#/$defs/McpServerCustomization" + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Schema-defined values. Unknown entries must survive round-trips." } - ], - "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." + }, + "required": [ + "id", + "kind", + "type", + "events" + ] }, - "Customization": { - "oneOf": [ - { - "$ref": "#/$defs/PluginCustomization" + "AutomationTriggerEventDefinition": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - { - "$ref": "#/$defs/DirectoryCustomization" + "title": { + "type": "string" }, - { - "$ref": "#/$defs/McpServerCustomization" + "description": { + "type": "string" } - ], - "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." + }, + "required": [ + "id", + "title" + ] }, - "McpServerState": { - "oneOf": [ - { - "$ref": "#/$defs/McpServerStartingState" + "AutomationTriggerDefinition": { + "type": "object", + "properties": { + "type": { + "type": "string" }, - { - "$ref": "#/$defs/McpServerReadyState" + "title": { + "type": "string" }, - { - "$ref": "#/$defs/McpServerAuthRequiredState" + "description": { + "type": "string" }, - { - "$ref": "#/$defs/McpServerErrorState" + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTriggerEventDefinition" + } }, - { - "$ref": "#/$defs/McpServerStoppedState" + "configSchema": { + "$ref": "#/$defs/ConfigSchema" } - ], - "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." + }, + "required": [ + "type", + "title", + "events" + ] }, - "ChatOrigin": { - "oneOf": [ - { - "type": "object", - "properties": { - "kind": { - "const": "user" - } + "AutomationSessionTemplate": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" }, - "required": [ - "kind" - ] + "description": "Absence means a workspace-less session." }, - { + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values resolved through `resolveSessionConfig`." + } + } + }, + "AutomationDefinition": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "$ref": "#/$defs/Message", + "description": "Initial user message sent to each new session." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate" + }, + "enabled": { + "type": "boolean", + "description": "Controls automatic triggers; manual runs remain permitted." + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + }, + "description": "Empty means manual-only." + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "title", + "message", + "session", + "enabled", + "triggers" + ] + }, + "AutomationRuntimeState": { + "type": "object", + "properties": { + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + } + }, + "AutomationSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "title": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "triggerCount": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "lastRun": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "revision": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition" + }, + "revision": { + "type": "number" + }, + "nextRunAt": { + "type": "string" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries." + }, + "runsNextCursor": { + "type": "string" + }, + "runtime": { + "$ref": "#/$defs/AutomationRuntimeState" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + } + }, + "createdAt": { + "type": "string" + }, + "modifiedAt": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationRunBlocker": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind" + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "properties": { + "kind": { + "const": "trigger" + }, + "triggerId": { + "type": "string" + }, + "scheduledFor": { + "type": "string" + }, + "catchUp": { + "type": "boolean" + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined event provenance containing no secrets." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "usage": { + "$ref": "#/$defs/UsageInfo" + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "AutomationFailedRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "failed" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "error": { + "$ref": "#/$defs/ErrorInfo" + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "AutomationCancelledRunLifecycle": { + "type": "object", + "properties": { + "status": { + "const": "cancelled" + }, + "createdAt": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "AutomationRunArtifact": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "AutomationRunSummary": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "sessionCount": { + "type": "number" + }, + "artifactCount": { + "type": "number" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "AutomationRunState": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/$defs/URI" + }, + "automation": { + "$ref": "#/$defs/URI" + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause" + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle" + }, + "sessions": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + } + }, + "primarySession": { + "$ref": "#/$defs/URI" + }, + "artifacts": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunArtifact" + } + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + } + }, + "_meta": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessions", + "artifacts", + "operations" + ] + }, + "StringOrMarkdown": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "markdown": { + "type": "string" + } + }, + "required": [ + "markdown" + ] + } + ], + "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." + }, + "JsonPrimitive": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "SessionOrigin": { + "$ref": "#/$defs/AutomationSessionOrigin" + }, + "SessionInputRequest": { + "oneOf": [ + { + "$ref": "#/$defs/SessionChatInputRequest" + }, + { + "$ref": "#/$defs/SessionToolConfirmationRequest" + }, + { + "$ref": "#/$defs/SessionToolClientExecutionRequest" + }, + { + "$ref": "#/$defs/SessionToolAuthenticationRequest" + } + ], + "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." + }, + "ChildCustomizationType": { + "oneOf": [ + { + "const": "agent" + }, + { + "const": "skill" + }, + { + "const": "prompt" + }, + { + "const": "rule" + }, + { + "const": "hook" + }, + { + "const": "mcpServer" + } + ], + "description": "Customization types that appear as children of a\n{@link PluginCustomization} or {@link DirectoryCustomization}." + }, + "CustomizationLoadState": { + "oneOf": [ + { + "$ref": "#/$defs/CustomizationLoadingState" + }, + { + "$ref": "#/$defs/CustomizationLoadedState" + }, + { + "$ref": "#/$defs/CustomizationDegradedState" + }, + { + "$ref": "#/$defs/CustomizationErrorState" + } + ], + "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "ChildCustomization": { + "oneOf": [ + { + "$ref": "#/$defs/AgentCustomization" + }, + { + "$ref": "#/$defs/SkillCustomization" + }, + { + "$ref": "#/$defs/PromptCustomization" + }, + { + "$ref": "#/$defs/RuleCustomization" + }, + { + "$ref": "#/$defs/HookCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." + }, + "Customization": { + "oneOf": [ + { + "$ref": "#/$defs/PluginCustomization" + }, + { + "$ref": "#/$defs/DirectoryCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "A top-level customization active in a session. Either a container\n({@link PluginCustomization} or {@link DirectoryCustomization}) whose\nleaf customizations live in its\n{@link ContainerCustomizationBase.children | `children`} array, or a\nbare {@link McpServerCustomization} surfaced directly by the host." + }, + "McpServerState": { + "oneOf": [ + { + "$ref": "#/$defs/McpServerStartingState" + }, + { + "$ref": "#/$defs/McpServerReadyState" + }, + { + "$ref": "#/$defs/McpServerAuthRequiredState" + }, + { + "$ref": "#/$defs/McpServerErrorState" + }, + { + "$ref": "#/$defs/McpServerStoppedState" + } + ], + "description": "Discriminated union of all MCP server lifecycle states.\nDiscriminated by `kind` (a {@link McpServerStatus} value)." + }, + "ChatOrigin": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "user" + } + }, + "required": [ + "kind" + ] + }, + { "type": "object", "properties": { "kind": { @@ -5321,6 +6044,64 @@ ], "description": "A content part within terminal output." }, + "AutomationSchedule": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationHourlySchedule" + }, + { + "$ref": "#/$defs/AutomationDailySchedule" + }, + { + "$ref": "#/$defs/AutomationWeeklySchedule" + }, + { + "$ref": "#/$defs/AutomationCronSchedule" + } + ] + }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$ref": "#/$defs/AutomationEventTrigger" + } + ] + }, + "AutomationRunCause": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ] + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ] + }, "URI": { "type": "string", "description": "A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)." @@ -5470,6 +6251,48 @@ ], "type": "string", "description": "Discriminant for {@link ResourceChange.type}." + }, + "AutomationWeekday": { + "enum": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "type": "string" + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string" + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "type": "string" + }, + "AutomationRunBlockerKind": { + "enum": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "type": "string" + }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string" } } } diff --git a/scripts/find-protocol-sources.ts b/scripts/find-protocol-sources.ts index 862088bd8..79fd2bdc1 100644 --- a/scripts/find-protocol-sources.ts +++ b/scripts/find-protocol-sources.ts @@ -24,6 +24,8 @@ export const PROTOCOL_SOURCE_DIRS: readonly string[] = [ 'channels-annotations', 'channels-otlp', 'channels-resource-watch', + 'channels-automation', + 'channels-automation-run', ]; /** diff --git a/scripts/generate-action-origin.ts b/scripts/generate-action-origin.ts index 0b66aa73d..b38eb2bb7 100644 --- a/scripts/generate-action-origin.ts +++ b/scripts/generate-action-origin.ts @@ -17,7 +17,7 @@ const GENERATED_HEADER = `// Generated from types/actions.ts — do not edit // Run \`npm run generate\` to regenerate. `; -type ActionScope = 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch'; +type ActionScope = 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch' | 'automation' | 'automationRun'; interface ActionInfo { /** The interface name (e.g. 'RootAgentsChangedAction') */ @@ -155,6 +155,8 @@ export function generateActionOrigin(project: Project, outDir: string): void { : category === 'Changeset Actions' ? 'changeset' : category === 'Annotations Actions' ? 'annotations' : category === 'Resource Watch Actions' ? 'resourceWatch' + : category === 'Automation Actions' ? 'automation' + : category === 'Automation Run Actions' ? 'automationRun' : 'session'; const isClientDispatchable = hasJsDocTag(node as any, 'clientDispatchable'); @@ -207,6 +209,8 @@ export function generateActionOrigin(project: Project, outDir: string): void { const changesetActions = actions.filter(a => a.scope === 'changeset'); const annotationsActions = actions.filter(a => a.scope === 'annotations'); const resourceWatchActions = actions.filter(a => a.scope === 'resourceWatch'); + const automationActions = actions.filter(a => a.scope === 'automation'); + const automationRunActions = actions.filter(a => a.scope === 'automationRun'); const clientRootActions = rootActions.filter(a => a.isClientDispatchable); const serverRootActions = rootActions.filter(a => !a.isClientDispatchable); const clientSessionActions = sessionActions.filter(a => a.isClientDispatchable); @@ -221,6 +225,10 @@ export function generateActionOrigin(project: Project, outDir: string): void { const serverAnnotationsActions = annotationsActions.filter(a => !a.isClientDispatchable); const clientResourceWatchActions = resourceWatchActions.filter(a => a.isClientDispatchable); const serverResourceWatchActions = resourceWatchActions.filter(a => !a.isClientDispatchable); + const clientAutomationActions = automationActions.filter(a => a.isClientDispatchable); + const serverAutomationActions = automationActions.filter(a => !a.isClientDispatchable); + const clientAutomationRunActions = automationRunActions.filter(a => a.isClientDispatchable); + const serverAutomationRunActions = automationRunActions.filter(a => !a.isClientDispatchable); const lines: string[] = [GENERATED_HEADER]; @@ -458,6 +466,60 @@ export function generateActionOrigin(project: Project, outDir: string): void { lines.push(`;`); lines.push(``); + // AutomationAction + lines.push(`/** Union of all automation-scoped actions. */`); + lines.push(`export type AutomationAction =`); + for (const a of automationActions) { + lines.push(` | ${a.name}`); + } + lines.push(`;`); + lines.push(``); + + lines.push(`/** Union of automation actions that clients may dispatch. */`); + lines.push(`export type ClientAutomationAction =`); + if (clientAutomationActions.length === 0) { + lines.push(` never`); + } else { + for (const a of clientAutomationActions) { + lines.push(` | ${a.name}`); + } + } + lines.push(`;`); + lines.push(``); + + lines.push(`/** Union of automation actions that only the server may produce. */`); + lines.push(`export type ServerAutomationAction =`); + for (const a of serverAutomationActions) { + lines.push(` | ${a.name}`); + } + lines.push(`;`); + lines.push(``); + + // AutomationRunAction + lines.push(`/** Union of all automation-run-scoped actions. */`); + lines.push(`export type AutomationRunAction =`); + for (const a of automationRunActions) { + lines.push(` | ${a.name}`); + } + lines.push(`;`); + lines.push(``); + + lines.push(`/** Union of automation-run actions that clients may dispatch. */`); + lines.push(`export type ClientAutomationRunAction =`); + for (const a of clientAutomationRunActions) { + lines.push(` | ${a.name}`); + } + lines.push(`;`); + lines.push(``); + + lines.push(`/** Union of automation-run actions that only the server may produce. */`); + lines.push(`export type ServerAutomationRunAction =`); + for (const a of serverAutomationRunActions) { + lines.push(` | ${a.name}`); + } + lines.push(`;`); + lines.push(``); + // IS_CLIENT_DISPATCHABLE map lines.push(`// ─── Client-Dispatchable Map ─────────────────────────────────────────────────`); diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 34e8eabd1..1e0b143d0 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -172,6 +172,7 @@ function mapType(tsType: string): string { tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState | AutomationState | AutomationRunState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState' ) { @@ -356,7 +357,9 @@ function extractProps(iface: InterfaceDeclaration, project: Project): GoProp[] { // token: optional null-able stays a single pointer (avoid `**T`). const alreadyPointer = goType.startsWith('*'); const optional = hasQuestionToken || hasUnionUndefined || alreadyPointer; - if (optional && !alreadyPointer && !goType.startsWith('[]') && !goType.startsWith('map[')) { + const presenceSensitiveCollection = iface.getName() === 'AutomationDefinitionPatch' + && (tsName === 'triggers' || tsName === '_meta'); + if (optional && !alreadyPointer && (presenceSensitiveCollection || (!goType.startsWith('[]') && !goType.startsWith('map[')))) { goType = `*${goType}`; } @@ -679,7 +682,20 @@ function generateDiscriminatedUnion(cfg: UnionConfig): string { lines.push('\tif u.Value == nil {'); lines.push('\t\treturn []byte("null"), nil'); lines.push('\t}'); - lines.push('\treturn json.Marshal(u.Value)'); + if (cfg.injectDiscriminantOnMarshal) { + lines.push('\tdata, err := json.Marshal(u.Value)'); + lines.push('\tif err != nil { return nil, err }'); + lines.push('\tvar object map[string]json.RawMessage'); + lines.push('\tif err := json.Unmarshal(data, &object); err != nil { return nil, err }'); + lines.push('\tswitch u.Value.(type) {'); + for (const v of cfg.variants) { + lines.push(`\tcase *${v.innerType}: object[${JSON.stringify(cfg.discriminantField)}] = json.RawMessage(${JSON.stringify(JSON.stringify(v.wireValue))})`); + } + lines.push('\t}'); + lines.push('\treturn json.Marshal(object)'); + } else { + lines.push('\treturn json.Marshal(u.Value)'); + } lines.push('}'); return lines.join('\n'); } @@ -698,6 +714,11 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', + 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [ @@ -829,6 +850,33 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, + { name: 'AutomationSessionOrigin' }, + { name: 'AutomationLocalTime' }, + { name: 'AutomationHourlySchedule' }, + { name: 'AutomationDailySchedule' }, + { name: 'AutomationWeeklySchedule' }, + { name: 'AutomationCronSchedule' }, + { name: 'AutomationScheduleTrigger' }, + { name: 'AutomationEventTrigger' }, + { name: 'AutomationTriggerEventDefinition' }, + { name: 'AutomationTriggerDefinition' }, + { name: 'AutomationSessionTemplate' }, + { name: 'AutomationDefinition' }, + { name: 'AutomationRuntimeState' }, + { name: 'AutomationSummary' }, + { name: 'AutomationState' }, + { name: 'AutomationRunBlocker' }, + { name: 'AutomationManualRunCause' }, + { name: 'AutomationTriggeredRunCause' }, + { name: 'AutomationPendingRunLifecycle' }, + { name: 'AutomationRunningRunLifecycle' }, + { name: 'AutomationBlockedRunLifecycle' }, + { name: 'AutomationCompletedRunLifecycle' }, + { name: 'AutomationFailedRunLifecycle' }, + { name: 'AutomationCancelledRunLifecycle' }, + { name: 'AutomationRunArtifact' }, + { name: 'AutomationRunSummary' }, + { name: 'AutomationRunState' }, ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -1054,6 +1102,66 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const SESSION_ORIGIN_UNION: UnionConfig = { + name: 'SessionOrigin', + discriminantField: 'kind', + doc: 'SessionOrigin is the durable origin of a session.', + variants: [ + { variantName: 'Automation', innerType: 'AutomationSessionOrigin', wireValue: 'automation' }, + ], + injectDiscriminantOnMarshal: true, +}; + +const AUTOMATION_SCHEDULE_UNION: UnionConfig = { + name: 'AutomationSchedule', + discriminantField: 'kind', + doc: 'AutomationSchedule is the calendar schedule for an automation trigger.', + variants: [ + { variantName: 'Hourly', innerType: 'AutomationHourlySchedule', wireValue: 'hourly' }, + { variantName: 'Daily', innerType: 'AutomationDailySchedule', wireValue: 'daily' }, + { variantName: 'Weekly', innerType: 'AutomationWeeklySchedule', wireValue: 'weekly' }, + { variantName: 'Cron', innerType: 'AutomationCronSchedule', wireValue: 'cron' }, + ], + injectDiscriminantOnMarshal: true, +}; + +const AUTOMATION_TRIGGER_UNION: UnionConfig = { + name: 'AutomationTrigger', + discriminantField: 'kind', + doc: 'AutomationTrigger is an automatic trigger for an automation.', + variants: [ + { variantName: 'Schedule', innerType: 'AutomationScheduleTrigger', wireValue: 'schedule' }, + { variantName: 'Event', innerType: 'AutomationEventTrigger', wireValue: 'event' }, + ], + injectDiscriminantOnMarshal: true, +}; + +const AUTOMATION_RUN_CAUSE_UNION: UnionConfig = { + name: 'AutomationRunCause', + discriminantField: 'kind', + doc: 'AutomationRunCause is the cause of an automation run.', + variants: [ + { variantName: 'Manual', innerType: 'AutomationManualRunCause', wireValue: 'manual' }, + { variantName: 'Trigger', innerType: 'AutomationTriggeredRunCause', wireValue: 'trigger' }, + ], + injectDiscriminantOnMarshal: true, +}; + +const AUTOMATION_RUN_LIFECYCLE_UNION: UnionConfig = { + name: 'AutomationRunLifecycle', + discriminantField: 'status', + doc: 'AutomationRunLifecycle is the lifecycle of an automation run.', + variants: [ + { variantName: 'Pending', innerType: 'AutomationPendingRunLifecycle', wireValue: 'pending' }, + { variantName: 'Running', innerType: 'AutomationRunningRunLifecycle', wireValue: 'running' }, + { variantName: 'Blocked', innerType: 'AutomationBlockedRunLifecycle', wireValue: 'blocked' }, + { variantName: 'Completed', innerType: 'AutomationCompletedRunLifecycle', wireValue: 'completed' }, + { variantName: 'Failed', innerType: 'AutomationFailedRunLifecycle', wireValue: 'failed' }, + { variantName: 'Cancelled', innerType: 'AutomationCancelledRunLifecycle', wireValue: 'cancelled' }, + ], + injectDiscriminantOnMarshal: true, +}; + function generateChatOriginGo(): string { return `// ChatOrigin describes how a chat came into existence. type ChatOrigin struct { @@ -1154,10 +1262,12 @@ func (o ChatOrigin) MarshalJSON() ([]byte, error) { function generateSnapshotState(): string { return `// SnapshotState is the state payload of a snapshot — root, session, -// chat, terminal, changeset, resource-watch, annotations, or content state. The active +// chat, terminal, changeset, resource-watch, annotations, automation, or +// automation-run state. The active // variant is chosen by which pointer field is non-nil; UnmarshalJSON probes // for required fields in the canonical order -// (session → chat → terminal → changeset → resourceWatch → annotations → root). +// (automationRun → automation → session → chat → terminal → changeset → +// resourceWatch → annotations → root). type SnapshotState struct { \tRoot *RootState \`json:"-"\` \tSession *SessionState \`json:"-"\` @@ -1166,11 +1276,17 @@ type SnapshotState struct { \tChangeset *ChangesetState \`json:"-"\` \tResourceWatch *ResourceWatchState \`json:"-"\` \tAnnotations *AnnotationsState \`json:"-"\` +\tAutomation *AutomationState \`json:"-"\` +\tAutomationRun *AutomationRunState \`json:"-"\` } // MarshalJSON encodes whichever variant is currently populated. func (s SnapshotState) MarshalJSON() ([]byte, error) { \tswitch { +\tcase s.AutomationRun != nil: +\t\treturn json.Marshal(s.AutomationRun) +\tcase s.Automation != nil: +\t\treturn json.Marshal(s.Automation) \tcase s.Session != nil: \t\treturn json.Marshal(s.Session) \tcase s.Chat != nil: @@ -1199,6 +1315,18 @@ func (s *SnapshotState) UnmarshalJSON(data []byte) error { \t\treturn err \t} \tswitch { +\tcase containsAll(probe, "automation", "cause", "sessions"): +\t\tvar v AutomationRunState +\t\tif err := json.Unmarshal(data, &v); err != nil { +\t\t\treturn err +\t\t} +\t\ts.AutomationRun = &v +\tcase containsAll(probe, "definition"): +\t\tvar v AutomationState +\t\tif err := json.Unmarshal(data, &v); err != nil { +\t\t\treturn err +\t\t} +\t\ts.Automation = &v \tcase containsAll(probe, "lifecycle"): \t\tvar v SessionState \t\tif err := json.Unmarshal(data, &v); err != nil { @@ -1353,6 +1481,16 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_LIFECYCLE_UNION)); + lines.push(''); lines.push(generateChatOriginGo()); lines.push(''); lines.push(generateSnapshotState()); @@ -1453,6 +1591,17 @@ const ACTION_VARIANTS: { { type: 'terminal/commandExecuted', variantName: 'TerminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', variantName: 'TerminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'automation/definitionChanged', variantName: 'AutomationDefinitionChanged', tsInterface: 'AutomationDefinitionChangedAction' }, + { type: 'automation/runSummarySet', variantName: 'AutomationRunSummarySet', tsInterface: 'AutomationRunSummarySetAction' }, + { type: 'automation/runSummaryRemoved', variantName: 'AutomationRunSummaryRemoved', tsInterface: 'AutomationRunSummaryRemovedAction' }, + { type: 'automation/runsLoaded', variantName: 'AutomationRunsLoaded', tsInterface: 'AutomationRunsLoadedAction' }, + { type: 'automationRun/lifecycleChanged', variantName: 'AutomationRunLifecycleChanged', tsInterface: 'AutomationRunLifecycleChangedAction' }, + { type: 'automationRun/sessionSet', variantName: 'AutomationRunSessionSet', tsInterface: 'AutomationRunSessionSetAction' }, + { type: 'automationRun/sessionRemoved', variantName: 'AutomationRunSessionRemoved', tsInterface: 'AutomationRunSessionRemovedAction' }, + { type: 'automationRun/primarySessionChanged', variantName: 'AutomationRunPrimarySessionChanged', tsInterface: 'AutomationRunPrimarySessionChangedAction' }, + { type: 'automationRun/artifactSet', variantName: 'AutomationRunArtifactSet', tsInterface: 'AutomationRunArtifactSetAction' }, + { type: 'automationRun/artifactRemoved', variantName: 'AutomationRunArtifactRemoved', tsInterface: 'AutomationRunArtifactRemovedAction' }, + { type: 'automationRun/cancelRequested', variantName: 'AutomationRunCancelRequested', tsInterface: 'AutomationRunCancelRequestedAction' }, ]; function generateMergedChatToolCallConfirmedStruct(): string { @@ -1558,7 +1707,11 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, - { name: 'ClientCapabilities' }, { name: 'Implementation' }, + { name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' }, + { name: 'AutomationExecutionCapabilities' }, { name: 'AutomationCreateCapability' }, + { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationCronScheduleCapability' }, + { name: 'AutomationRunCancellationCapability' }, { name: 'AutomationSchedulePreviewCapability' }, + { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, @@ -1588,6 +1741,13 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'CompletionsParams' }, { name: 'CompletionItem' }, { name: 'CompletionsResult' }, { name: 'InvokeChangesetOperationParams' }, { name: 'InvokeChangesetOperationResult' }, { name: 'ChangesetOperationFollowUp' }, + { name: 'ListAutomationsParams' }, { name: 'ListAutomationsResult' }, + { name: 'ListAutomationTriggerDefinitionsParams' }, { name: 'ListAutomationTriggerDefinitionsResult' }, + { name: 'CreateAutomationParams' }, { name: 'AutomationDefinitionPatch' }, + { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, + { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, + { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, + { name: 'PreviewAutomationScheduleParams' }, { name: 'PreviewAutomationScheduleResult' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1731,6 +1891,9 @@ const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'AutomationAddedParams', + 'AutomationRemovedParams', + 'AutomationSummaryChangedParams', 'ProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', @@ -2086,6 +2249,11 @@ function checkExhaustiveness(project: Project): void { 'SessionInputRequest', 'ToolCallConfirmationState', 'ReconnectResult', + 'SessionOrigin', + 'AutomationSchedule', + 'AutomationTrigger', + 'AutomationRunCause', + 'AutomationRunLifecycle', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index 7912b6d3e..dc3332576 100644 --- a/scripts/generate-json-schema.test.ts +++ b/scripts/generate-json-schema.test.ts @@ -187,6 +187,20 @@ describe('generated JSON schemas', () => { assert.match(baseProperties._meta.description as string, /Receivers MUST ignore keys/); }); + it('preserves nested automation capability objects', () => { + if (file !== 'commands.schema.json') { + return; + } + const defs = schema.$defs as Record>; + const schedules = defs.AutomationScheduleCapabilities; + const properties = schedules.properties as Record>; + assert.deepEqual(properties.cron.$ref, '#/$defs/AutomationCronScheduleCapability'); + const cron = defs.AutomationCronScheduleCapability; + const cronProperties = cron.properties as Record>; + assert.deepEqual(cronProperties.dialect.enum, ['unix5']); + assert.equal(cronProperties.minIntervalMinutes.type, 'number'); + }); + it('constrains every ChatOrigin branch to a distinct kind', () => { const defs = schema.$defs as Record>; const chatOrigin = defs.ChatOrigin; diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 81a192deb..251e1e21e 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -54,6 +54,7 @@ const GENERATED_HEADER = 'import kotlinx.serialization.json.JsonObject\n' + 'import kotlinx.serialization.json.JsonPrimitive\n' + 'import kotlinx.serialization.json.buildJsonObject\n' + + 'import kotlinx.serialization.json.jsonObject\n' + 'import kotlinx.serialization.json.contentOrNull\n'; const PACKAGE = 'com.microsoft.agenthostprotocol.generated'; @@ -86,7 +87,7 @@ function snakeToCamel(s: string): string { const KOTLIN_RESERVED_KEYWORDS = new Set([ // Hard keywords 'as', 'break', 'class', 'continue', 'do', 'else', 'false', 'for', 'fun', - 'if', 'in', 'interface', 'is', 'null', 'object', 'package', 'return', + 'if', 'import', 'in', 'interface', 'is', 'null', 'object', 'package', 'return', 'super', 'this', 'throw', 'true', 'try', 'typealias', 'typeof', 'val', 'var', 'when', 'while', ]); @@ -148,6 +149,7 @@ function mapType(tsType: string): string { tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' || + tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState | AutomationState | AutomationRunState' || tsType === 'RootState | SessionState | ChatState' || tsType === 'RootState | SessionState | ChatState | TerminalState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' || @@ -483,6 +485,8 @@ interface UnionConfig { * on the same set of state-channel unions. */ unknown?: boolean; + /** Force the sealed case's discriminator when serializing its payload. */ + injectDiscriminantOnSerialize?: boolean; } /** @@ -570,7 +574,21 @@ function generateDiscriminatedUnion(config: UnionConfig): string { lines.push(` is ${config.name}Unknown -> value.raw`); } lines.push(' }'); - lines.push(' output.encodeJsonElement(element)'); + if (config.injectDiscriminantOnSerialize) { + lines.push(' val encodedObject = element.jsonObject.toMutableMap()'); + lines.push(' val discriminant = when (value) {'); + for (const v of byStruct.values()) { + lines.push(` is ${config.name}${v.caseName} -> ${JSON.stringify(v.discriminantValue)}`); + } + if (config.unknown) { + lines.push(` is ${config.name}Unknown -> null`); + } + lines.push(' }'); + lines.push(` if (discriminant != null) encodedObject[${JSON.stringify(config.discriminantField)}] = JsonPrimitive(discriminant)`); + lines.push(' output.encodeJsonElement(JsonObject(encodedObject))'); + } else { + lines.push(' output.encodeJsonElement(element)'); + } lines.push(' }'); lines.push('}'); @@ -767,8 +785,7 @@ internal object ToolInputSerializer : KSerializer { function generateSnapshotState(): string { return `/** - * The state payload of a snapshot — root, session, chat, terminal, changeset, - * resource-watch, annotations, or content state. + * The state payload of a snapshot. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { @@ -779,6 +796,8 @@ sealed interface SnapshotState { @JvmInline value class Changeset(val value: ChangesetState) : SnapshotState @JvmInline value class ResourceWatch(val value: ResourceWatchState) : SnapshotState @JvmInline value class Annotations(val value: AnnotationsState) : SnapshotState + @JvmInline value class Automation(val value: AutomationState) : SnapshotState + @JvmInline value class AutomationRun(val value: AutomationRunState) : SnapshotState } internal object SnapshotStateSerializer : KSerializer { @@ -791,7 +810,9 @@ internal object SnapshotStateSerializer : KSerializer { val element = input.decodeJsonElement() val obj = element as? JsonObject ?: error("Expected JsonObject for SnapshotState") - // Try the most distinctive shape first. SessionState has required + // Try the most distinctive shape first. AutomationRunState has required + // \`automation\`, \`cause\`, and \`sessions\`; AutomationState has required + // \`definition\`; SessionState has required // \`lifecycle\`; ChatState has required \`turns\`; ChangesetState has // required \`status\` + \`files\`; ResourceWatchState has required // \`root\` + \`recursive\`; AnnotationsState has required \`annotations\` @@ -799,6 +820,10 @@ internal object SnapshotStateSerializer : KSerializer { // key); TerminalState has required \`content\`; RootState is the // catch-all. return when { + obj.containsKey("automation") && obj.containsKey("cause") && obj.containsKey("sessions") -> + SnapshotState.AutomationRun(input.json.decodeFromJsonElement(AutomationRunState.serializer(), element)) + obj.containsKey("definition") -> + SnapshotState.Automation(input.json.decodeFromJsonElement(AutomationState.serializer(), element)) obj.containsKey("lifecycle") -> SnapshotState.Session(input.json.decodeFromJsonElement(SessionState.serializer(), element)) obj.containsKey("turns") -> SnapshotState.Chat(input.json.decodeFromJsonElement(ChatState.serializer(), element)) obj.containsKey("status") && obj.containsKey("files") -> @@ -824,6 +849,8 @@ internal object SnapshotStateSerializer : KSerializer { is SnapshotState.Changeset -> output.json.encodeToJsonElement(ChangesetState.serializer(), value.value) is SnapshotState.ResourceWatch -> output.json.encodeToJsonElement(ResourceWatchState.serializer(), value.value) is SnapshotState.Annotations -> output.json.encodeToJsonElement(AnnotationsState.serializer(), value.value) + is SnapshotState.Automation -> output.json.encodeToJsonElement(AutomationState.serializer(), value.value) + is SnapshotState.AutomationRun -> output.json.encodeToJsonElement(AutomationRunState.serializer(), value.value) } output.encodeJsonElement(element) } @@ -903,6 +930,11 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', + 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; const STATE_STRUCTS = [ @@ -958,6 +990,18 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', + 'AutomationSessionOrigin', + 'AutomationLocalTime', 'AutomationHourlySchedule', 'AutomationDailySchedule', + 'AutomationWeeklySchedule', 'AutomationCronSchedule', + 'AutomationScheduleTrigger', 'AutomationEventTrigger', + 'AutomationTriggerEventDefinition', 'AutomationTriggerDefinition', + 'AutomationSessionTemplate', 'AutomationDefinition', 'AutomationRuntimeState', + 'AutomationSummary', 'AutomationState', + 'AutomationRunBlocker', 'AutomationManualRunCause', 'AutomationTriggeredRunCause', + 'AutomationPendingRunLifecycle', 'AutomationRunningRunLifecycle', + 'AutomationBlockedRunLifecycle', 'AutomationCompletedRunLifecycle', + 'AutomationFailedRunLifecycle', 'AutomationCancelledRunLifecycle', + 'AutomationRunArtifact', 'AutomationRunSummary', 'AutomationRunState', ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -1221,6 +1265,61 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const SESSION_ORIGIN_UNION: UnionConfig = { + name: 'SessionOrigin', + discriminantField: 'kind', + variants: [ + { caseName: 'Automation', structName: 'AutomationSessionOrigin', discriminantValue: 'automation' }, + ], + injectDiscriminantOnSerialize: true, +}; + +const AUTOMATION_SCHEDULE_UNION: UnionConfig = { + name: 'AutomationSchedule', + discriminantField: 'kind', + variants: [ + { caseName: 'Hourly', structName: 'AutomationHourlySchedule', discriminantValue: 'hourly' }, + { caseName: 'Daily', structName: 'AutomationDailySchedule', discriminantValue: 'daily' }, + { caseName: 'Weekly', structName: 'AutomationWeeklySchedule', discriminantValue: 'weekly' }, + { caseName: 'Cron', structName: 'AutomationCronSchedule', discriminantValue: 'cron' }, + ], + injectDiscriminantOnSerialize: true, +}; + +const AUTOMATION_TRIGGER_UNION: UnionConfig = { + name: 'AutomationTrigger', + discriminantField: 'kind', + variants: [ + { caseName: 'Schedule', structName: 'AutomationScheduleTrigger', discriminantValue: 'schedule' }, + { caseName: 'Event', structName: 'AutomationEventTrigger', discriminantValue: 'event' }, + ], + injectDiscriminantOnSerialize: true, +}; + +const AUTOMATION_RUN_CAUSE_UNION: UnionConfig = { + name: 'AutomationRunCause', + discriminantField: 'kind', + variants: [ + { caseName: 'Manual', structName: 'AutomationManualRunCause', discriminantValue: 'manual' }, + { caseName: 'Trigger', structName: 'AutomationTriggeredRunCause', discriminantValue: 'trigger' }, + ], + injectDiscriminantOnSerialize: true, +}; + +const AUTOMATION_RUN_LIFECYCLE_UNION: UnionConfig = { + name: 'AutomationRunLifecycle', + discriminantField: 'status', + variants: [ + { caseName: 'Pending', structName: 'AutomationPendingRunLifecycle', discriminantValue: 'pending' }, + { caseName: 'Running', structName: 'AutomationRunningRunLifecycle', discriminantValue: 'running' }, + { caseName: 'Blocked', structName: 'AutomationBlockedRunLifecycle', discriminantValue: 'blocked' }, + { caseName: 'Completed', structName: 'AutomationCompletedRunLifecycle', discriminantValue: 'completed' }, + { caseName: 'Failed', structName: 'AutomationFailedRunLifecycle', discriminantValue: 'failed' }, + { caseName: 'Cancelled', structName: 'AutomationCancelledRunLifecycle', discriminantValue: 'cancelled' }, + ], + injectDiscriminantOnSerialize: true, +}; + function generateStateFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; @@ -1297,6 +1396,16 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_LIFECYCLE_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -1393,6 +1502,17 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'terminal/commandExecuted', caseName: 'TerminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', caseName: 'TerminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', caseName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'automation/definitionChanged', caseName: 'AutomationDefinitionChanged', tsInterface: 'AutomationDefinitionChangedAction' }, + { type: 'automation/runSummarySet', caseName: 'AutomationRunSummarySet', tsInterface: 'AutomationRunSummarySetAction' }, + { type: 'automation/runSummaryRemoved', caseName: 'AutomationRunSummaryRemoved', tsInterface: 'AutomationRunSummaryRemovedAction' }, + { type: 'automation/runsLoaded', caseName: 'AutomationRunsLoaded', tsInterface: 'AutomationRunsLoadedAction' }, + { type: 'automationRun/lifecycleChanged', caseName: 'AutomationRunLifecycleChanged', tsInterface: 'AutomationRunLifecycleChangedAction' }, + { type: 'automationRun/sessionSet', caseName: 'AutomationRunSessionSet', tsInterface: 'AutomationRunSessionSetAction' }, + { type: 'automationRun/sessionRemoved', caseName: 'AutomationRunSessionRemoved', tsInterface: 'AutomationRunSessionRemovedAction' }, + { type: 'automationRun/primarySessionChanged', caseName: 'AutomationRunPrimarySessionChanged', tsInterface: 'AutomationRunPrimarySessionChangedAction' }, + { type: 'automationRun/artifactSet', caseName: 'AutomationRunArtifactSet', tsInterface: 'AutomationRunArtifactSetAction' }, + { type: 'automationRun/artifactRemoved', caseName: 'AutomationRunArtifactRemoved', tsInterface: 'AutomationRunArtifactRemovedAction' }, + { type: 'automationRun/cancelRequested', caseName: 'AutomationRunCancelRequested', tsInterface: 'AutomationRunCancelRequestedAction' }, ]; /** Merged data class for the approved/denied tool call confirmed action. */ @@ -1553,7 +1673,11 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', - 'ClientCapabilities', 'Implementation', + 'ClientCapabilities', 'AutomationCapabilities', + 'AutomationExecutionCapabilities', 'AutomationCreateCapability', + 'AutomationScheduleCapabilities', 'AutomationCronScheduleCapability', + 'AutomationRunCancellationCapability', 'AutomationSchedulePreviewCapability', + 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', @@ -1580,6 +1704,12 @@ const COMMAND_STRUCTS = [ 'CompletionsParams', 'CompletionItem', 'CompletionsResult', 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', + 'ListAutomationsParams', 'ListAutomationsResult', + 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', + 'CreateAutomationParams', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', + 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', + 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1742,6 +1872,9 @@ const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'AutomationAddedParams', + 'AutomationRemovedParams', + 'AutomationSummaryChangedParams', 'ProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', @@ -2117,6 +2250,11 @@ function checkExhaustiveness(project: Project): void { 'AhpErrorCodeWithData', // type-level alias; not a Kotlin type 'JsonRpcErrorCode', // type-level alias over JsonRpcErrorCodes const enum 'ReconnectResult', // RECONNECT_RESULT_UNION discriminated union + 'SessionOrigin', // SESSION_ORIGIN_UNION discriminated union + 'AutomationSchedule', // AUTOMATION_SCHEDULE_UNION discriminated union + 'AutomationTrigger', // AUTOMATION_TRIGGER_UNION discriminated union + 'AutomationRunCause', // AUTOMATION_RUN_CAUSE_UNION discriminated union + 'AutomationRunLifecycle', // AUTOMATION_RUN_LIFECYCLE_UNION discriminated union 'ForkChatSource', // generateFixedChatSourceBranchKotlin() 'SideChatSource', // generateFixedChatSourceBranchKotlin() 'ChangesetOperationTarget', // generateChangesetOperationTargetKotlin() diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts index 38153690f..88f1ad911 100644 --- a/scripts/generate-markdown.ts +++ b/scripts/generate-markdown.ts @@ -56,6 +56,8 @@ const DIR_TO_PAGE: Record = { 'channels-changeset': 'changeset', 'channels-annotations': 'annotations', 'channels-otlp': 'otlp', + 'channels-automation': 'automation', + 'channels-automation-run': 'automation-run', }; /** @@ -1291,6 +1293,38 @@ function generateMessagesPage(project: Project): string { return lines.join('\n'); } +function generateAutomationChannelPage(project: Project): string { + currentPage = 'automation'; + const stateSf = findChannelSourceFile(project, 'channels-automation', 'state.ts'); + const actionsSf = findChannelSourceFile(project, 'channels-automation', 'actions.ts'); + const commandsSf = findChannelSourceFile(project, 'channels-automation', 'commands.ts'); + const lines: string[] = [GENERATED_HEADER, '# Automation Channel\n', schemaLink('state.schema.json')]; + if (stateSf) { + lines.push('## State Types\n', emitStateTypesSection([stateSf])); + } + if (actionsSf) { + lines.push('## Actions\n', schemaLink('actions.schema.json'), emitActionsSection([actionsSf])); + } + if (commandsSf) { + lines.push('## Commands\n', schemaLink('commands.schema.json'), emitCommandsSection(project, [commandsSf])); + } + return lines.join('\n'); +} + +function generateAutomationRunChannelPage(project: Project): string { + currentPage = 'automation-run'; + const stateSf = findChannelSourceFile(project, 'channels-automation-run', 'state.ts'); + const actionsSf = findChannelSourceFile(project, 'channels-automation-run', 'actions.ts'); + const lines: string[] = [GENERATED_HEADER, '# Automation Run Channel\n', schemaLink('state.schema.json')]; + if (stateSf) { + lines.push('## State Types\n', emitStateTypesSection([stateSf])); + } + if (actionsSf) { + lines.push('## Actions\n', schemaLink('actions.schema.json'), emitActionsSection([actionsSf])); + } + return lines.join('\n'); +} + // ─── Public API ────────────────────────────────────────────────────────────── export function generateMarkdownDocs(project: Project, outDir: string): void { @@ -1308,6 +1342,8 @@ export function generateMarkdownDocs(project: Project, outDir: string): void { { filename: 'changeset.md', generator: generateChangesetChannelPage }, { filename: 'annotations.md', generator: generateAnnotationsChannelPage }, { filename: 'otlp.md', generator: generateOtlpChannelPage }, + { filename: 'automation.md', generator: generateAutomationChannelPage }, + { filename: 'automation-run.md', generator: generateAutomationRunChannelPage }, { filename: 'messages.md', generator: generateMessagesPage }, { filename: 'error-codes.md', generator: generateErrorCodesPage }, ]; diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 58a3bb9e7..098471206 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -159,6 +159,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' + || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState | AutomationState | AutomationRunState' || tsType === 'RootState | SessionState | ChatState' || tsType === 'RootState | SessionState | ChatState | TerminalState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' @@ -660,6 +661,11 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', + 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; /** @@ -812,6 +818,33 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, + { name: 'AutomationSessionOrigin', omitDiscriminants: true }, + { name: 'AutomationLocalTime' }, + { name: 'AutomationHourlySchedule', omitDiscriminants: true }, + { name: 'AutomationDailySchedule', omitDiscriminants: true }, + { name: 'AutomationWeeklySchedule', omitDiscriminants: true }, + { name: 'AutomationCronSchedule', omitDiscriminants: true }, + { name: 'AutomationScheduleTrigger', omitDiscriminants: true }, + { name: 'AutomationEventTrigger', omitDiscriminants: true }, + { name: 'AutomationTriggerEventDefinition' }, + { name: 'AutomationTriggerDefinition' }, + { name: 'AutomationSessionTemplate' }, + { name: 'AutomationDefinition' }, + { name: 'AutomationRuntimeState' }, + { name: 'AutomationSummary' }, + { name: 'AutomationState' }, + { name: 'AutomationRunBlocker' }, + { name: 'AutomationManualRunCause', omitDiscriminants: true }, + { name: 'AutomationTriggeredRunCause', omitDiscriminants: true }, + { name: 'AutomationPendingRunLifecycle', omitDiscriminants: true }, + { name: 'AutomationRunningRunLifecycle', omitDiscriminants: true }, + { name: 'AutomationBlockedRunLifecycle', omitDiscriminants: true }, + { name: 'AutomationCompletedRunLifecycle', omitDiscriminants: true }, + { name: 'AutomationFailedRunLifecycle', omitDiscriminants: true }, + { name: 'AutomationCancelledRunLifecycle', omitDiscriminants: true }, + { name: 'AutomationRunArtifact' }, + { name: 'AutomationRunSummary' }, + { name: 'AutomationRunState' }, ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -1042,6 +1075,61 @@ const SESSION_INPUT_REQUEST_UNION: UnionConfig = { unknown: true, }; +const SESSION_ORIGIN_UNION: UnionConfig = { + name: 'SessionOrigin', + discriminantField: 'kind', + doc: 'Durable origin of a session.', + variants: [ + { variantName: 'Automation', innerType: 'AutomationSessionOrigin', wireValue: 'automation' }, + ], +}; + +const AUTOMATION_SCHEDULE_UNION: UnionConfig = { + name: 'AutomationSchedule', + discriminantField: 'kind', + doc: 'Calendar schedule for an automation trigger.', + variants: [ + { variantName: 'Hourly', innerType: 'AutomationHourlySchedule', wireValue: 'hourly' }, + { variantName: 'Daily', innerType: 'AutomationDailySchedule', wireValue: 'daily' }, + { variantName: 'Weekly', innerType: 'AutomationWeeklySchedule', wireValue: 'weekly' }, + { variantName: 'Cron', innerType: 'AutomationCronSchedule', wireValue: 'cron' }, + ], +}; + +const AUTOMATION_TRIGGER_UNION: UnionConfig = { + name: 'AutomationTrigger', + discriminantField: 'kind', + doc: 'Automatic trigger for an automation.', + variants: [ + { variantName: 'Schedule', innerType: 'AutomationScheduleTrigger', wireValue: 'schedule' }, + { variantName: 'Event', innerType: 'AutomationEventTrigger', wireValue: 'event' }, + ], +}; + +const AUTOMATION_RUN_CAUSE_UNION: UnionConfig = { + name: 'AutomationRunCause', + discriminantField: 'kind', + doc: 'Cause of an automation run.', + variants: [ + { variantName: 'Manual', innerType: 'AutomationManualRunCause', wireValue: 'manual' }, + { variantName: 'Trigger', innerType: 'AutomationTriggeredRunCause', wireValue: 'trigger' }, + ], +}; + +const AUTOMATION_RUN_LIFECYCLE_UNION: UnionConfig = { + name: 'AutomationRunLifecycle', + discriminantField: 'status', + doc: 'Lifecycle of an automation run.', + variants: [ + { variantName: 'Pending', innerType: 'AutomationPendingRunLifecycle', wireValue: 'pending' }, + { variantName: 'Running', innerType: 'AutomationRunningRunLifecycle', wireValue: 'running' }, + { variantName: 'Blocked', innerType: 'AutomationBlockedRunLifecycle', wireValue: 'blocked' }, + { variantName: 'Completed', innerType: 'AutomationCompletedRunLifecycle', wireValue: 'completed' }, + { variantName: 'Failed', innerType: 'AutomationFailedRunLifecycle', wireValue: 'failed' }, + { variantName: 'Cancelled', innerType: 'AutomationCancelledRunLifecycle', wireValue: 'cancelled' }, + ], +}; + function generateChatOrigin(): string { return `/// How a chat came into existence. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -1088,8 +1176,7 @@ pub enum ChatOrigin { } function generateSnapshotState(): string { - return `/// The state payload of a snapshot — root, session, chat, terminal, -/// changeset, resource-watch, annotations, or content state. + return `/// The state payload of a snapshot. /// /// Deserialized by trying session first (has required \`lifecycle\`), then /// chat (has required \`turns\`), then terminal (has required \`content\`), @@ -1105,6 +1192,8 @@ pub enum SnapshotState { Changeset(Box), ResourceWatch(Box), Annotations(Box), + Automation(Box), + AutomationRun(Box), Root(Box), }`; } @@ -1191,6 +1280,16 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_LIFECYCLE_UNION)); + lines.push(''); lines.push(generateSnapshotState()); lines.push(''); @@ -1292,6 +1391,17 @@ const ACTION_VARIANTS: { { type: 'terminal/commandExecuted', variantName: 'TerminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', variantName: 'TerminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', variantName: 'ResourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'automation/definitionChanged', variantName: 'AutomationDefinitionChanged', tsInterface: 'AutomationDefinitionChangedAction', boxed: true }, + { type: 'automation/runSummarySet', variantName: 'AutomationRunSummarySet', tsInterface: 'AutomationRunSummarySetAction', boxed: true }, + { type: 'automation/runSummaryRemoved', variantName: 'AutomationRunSummaryRemoved', tsInterface: 'AutomationRunSummaryRemovedAction' }, + { type: 'automation/runsLoaded', variantName: 'AutomationRunsLoaded', tsInterface: 'AutomationRunsLoadedAction', boxed: true }, + { type: 'automationRun/lifecycleChanged', variantName: 'AutomationRunLifecycleChanged', tsInterface: 'AutomationRunLifecycleChangedAction', boxed: true }, + { type: 'automationRun/sessionSet', variantName: 'AutomationRunSessionSet', tsInterface: 'AutomationRunSessionSetAction' }, + { type: 'automationRun/sessionRemoved', variantName: 'AutomationRunSessionRemoved', tsInterface: 'AutomationRunSessionRemovedAction' }, + { type: 'automationRun/primarySessionChanged', variantName: 'AutomationRunPrimarySessionChanged', tsInterface: 'AutomationRunPrimarySessionChangedAction' }, + { type: 'automationRun/artifactSet', variantName: 'AutomationRunArtifactSet', tsInterface: 'AutomationRunArtifactSetAction', boxed: true }, + { type: 'automationRun/artifactRemoved', variantName: 'AutomationRunArtifactRemoved', tsInterface: 'AutomationRunArtifactRemovedAction' }, + { type: 'automationRun/cancelRequested', variantName: 'AutomationRunCancelRequested', tsInterface: 'AutomationRunCancelRequestedAction' }, ]; function generateMergedToolCallConfirmedStruct(scope: 'Session' | 'Chat' = 'Session'): string { @@ -1330,7 +1440,7 @@ pub struct ${scope}ToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, ErrorInfo, 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, AutomationDefinition, AutomationRunArtifact, AutomationRunLifecycle, AutomationRunOperation, AutomationRunSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, ErrorInfo, 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 @@ -1433,7 +1543,11 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, - { name: 'ClientCapabilities' }, { name: 'Implementation' }, + { name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' }, + { name: 'AutomationExecutionCapabilities' }, { name: 'AutomationCreateCapability' }, + { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationCronScheduleCapability' }, + { name: 'AutomationRunCancellationCapability' }, { name: 'AutomationSchedulePreviewCapability' }, + { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, @@ -1464,6 +1578,13 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'CompletionsParams' }, { name: 'CompletionItem' }, { name: 'CompletionsResult' }, { name: 'InvokeChangesetOperationParams' }, { name: 'InvokeChangesetOperationResult' }, { name: 'ChangesetOperationFollowUp' }, + { name: 'ListAutomationsParams' }, { name: 'ListAutomationsResult' }, + { name: 'ListAutomationTriggerDefinitionsParams' }, { name: 'ListAutomationTriggerDefinitionsResult' }, + { name: 'CreateAutomationParams' }, { name: 'AutomationDefinitionPatch' }, + { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, + { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, + { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, + { name: 'PreviewAutomationScheduleParams' }, { name: 'PreviewAutomationScheduleResult' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1491,7 +1612,7 @@ function generateCommandsFile(project: Project): string { lines.push('#[allow(unused_imports)]'); lines.push('use crate::actions::{ActionEnvelope, StateAction};'); lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentSelection, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); + lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationExecutionLifetime, AutomationSchedule, AutomationScheduleKind, AutomationSessionTemplate, AutomationSummary, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); @@ -1601,6 +1722,9 @@ const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'AutomationAddedParams', + 'AutomationRemovedParams', + 'AutomationSummaryChangedParams', 'ProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', @@ -1611,7 +1735,7 @@ const NOTIFICATION_STRUCTS = [ function generateNotificationsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, SessionStatus, SessionSummary};'); + lines.push('use crate::state::{AgentSelection, AnnotationsSummary, AutomationOperation, AutomationRunSummary, AutomationSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, SessionOrigin, SessionStatus, SessionSummary};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); @@ -1918,6 +2042,11 @@ function checkExhaustiveness(project: Project): void { 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ReconnectResult', + 'SessionOrigin', + 'AutomationSchedule', + 'AutomationTrigger', + 'AutomationRunCause', + 'AutomationRunLifecycle', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 6b486893e..406ad9fc7 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -115,6 +115,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str || tsType === 'RootState | SessionState | TerminalState | ChangesetState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState' || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState' + || tsType === 'RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState | AutomationState | AutomationRunState' || tsType === 'RootState | SessionState | ChatState' || tsType === 'RootState | SessionState | ChatState | TerminalState' || tsType === 'RootState | SessionState | ChatState | TerminalState | ChangesetState' @@ -446,6 +447,8 @@ interface UnionConfig { * ChangesetOperationTarget, ReconnectResult). */ allowUnknown?: boolean; + /** Force the enum case's discriminator when encoding its payload. */ + injectDiscriminantOnEncode?: boolean; } function generateDiscriminatedUnion(config: UnionConfig): string { @@ -490,7 +493,13 @@ function generateDiscriminatedUnion(config: UnionConfig): string { lines.push(' public func encode(to encoder: Encoder) throws {'); lines.push(' switch self {'); for (const v of config.variants) { - lines.push(` case .${v.caseName}(let value): try value.encode(to: encoder)`); + if (config.injectDiscriminantOnEncode) { + lines.push(` case .${v.caseName}(var value):`); + lines.push(` value.${config.discriminantField} = .${v.caseName}`); + lines.push(' try value.encode(to: encoder)'); + } else { + lines.push(` case .${v.caseName}(let value): try value.encode(to: encoder)`); + } } if (config.allowUnknown) { lines.push(' case .unknown(let value): try value.encode(to: encoder)'); @@ -612,6 +621,11 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', + 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; const STATE_STRUCTS = [ @@ -667,6 +681,18 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', + 'AutomationSessionOrigin', + 'AutomationLocalTime', 'AutomationHourlySchedule', 'AutomationDailySchedule', + 'AutomationWeeklySchedule', 'AutomationCronSchedule', + 'AutomationScheduleTrigger', 'AutomationEventTrigger', + 'AutomationTriggerEventDefinition', 'AutomationTriggerDefinition', + 'AutomationSessionTemplate', 'AutomationDefinition', 'AutomationRuntimeState', + 'AutomationSummary', 'AutomationState', + 'AutomationRunBlocker', 'AutomationManualRunCause', 'AutomationTriggeredRunCause', + 'AutomationPendingRunLifecycle', 'AutomationRunningRunLifecycle', + 'AutomationBlockedRunLifecycle', 'AutomationCompletedRunLifecycle', + 'AutomationFailedRunLifecycle', 'AutomationCancelledRunLifecycle', + 'AutomationRunArtifact', 'AutomationRunSummary', 'AutomationRunState', ]; const RESPONSE_PART_UNION: UnionConfig = { @@ -995,7 +1021,7 @@ public enum ToolInput: Codable, Sendable { } function generateSnapshotState(): string { - return `/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, annotations, or content state. + return `/// The state payload of a snapshot. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) @@ -1004,6 +1030,8 @@ public enum SnapshotState: Codable, Sendable { case changeset(ChangesetState) case resourceWatch(ResourceWatchState) case annotations(AnnotationsState) + case automation(AutomationState) + case automationRun(AutomationRunState) public init(from decoder: Decoder) throws { // Try the most distinctive shapes first. SessionState has required @@ -1022,6 +1050,10 @@ public enum SnapshotState: Codable, Sendable { self = .resourceWatch(resourceWatch) } else if let annotations = try? AnnotationsState(from: decoder) { self = .annotations(annotations) + } else if let automation = try? AutomationState(from: decoder) { + self = .automation(automation) + } else if let automationRun = try? AutomationRunState(from: decoder) { + self = .automationRun(automationRun) } else { self = .root(try RootState(from: decoder)) } @@ -1036,6 +1068,8 @@ public enum SnapshotState: Codable, Sendable { case .changeset(let state): try state.encode(to: encoder) case .resourceWatch(let state): try state.encode(to: encoder) case .annotations(let state): try state.encode(to: encoder) + case .automation(let state): try state.encode(to: encoder) + case .automationRun(let state): try state.encode(to: encoder) } } }`; @@ -1121,6 +1155,61 @@ public enum ChatOrigin: Codable, Sendable { }`; } +const SESSION_ORIGIN_UNION: UnionConfig = { + name: 'SessionOrigin', + discriminantField: 'kind', + variants: [ + { caseName: 'automation', structName: 'AutomationSessionOrigin', discriminantValue: 'automation' }, + ], + injectDiscriminantOnEncode: true, +}; + +const AUTOMATION_SCHEDULE_UNION: UnionConfig = { + name: 'AutomationSchedule', + discriminantField: 'kind', + variants: [ + { caseName: 'hourly', structName: 'AutomationHourlySchedule', discriminantValue: 'hourly' }, + { caseName: 'daily', structName: 'AutomationDailySchedule', discriminantValue: 'daily' }, + { caseName: 'weekly', structName: 'AutomationWeeklySchedule', discriminantValue: 'weekly' }, + { caseName: 'cron', structName: 'AutomationCronSchedule', discriminantValue: 'cron' }, + ], + injectDiscriminantOnEncode: true, +}; + +const AUTOMATION_TRIGGER_UNION: UnionConfig = { + name: 'AutomationTrigger', + discriminantField: 'kind', + variants: [ + { caseName: 'schedule', structName: 'AutomationScheduleTrigger', discriminantValue: 'schedule' }, + { caseName: 'event', structName: 'AutomationEventTrigger', discriminantValue: 'event' }, + ], + injectDiscriminantOnEncode: true, +}; + +const AUTOMATION_RUN_CAUSE_UNION: UnionConfig = { + name: 'AutomationRunCause', + discriminantField: 'kind', + variants: [ + { caseName: 'manual', structName: 'AutomationManualRunCause', discriminantValue: 'manual' }, + { caseName: 'trigger', structName: 'AutomationTriggeredRunCause', discriminantValue: 'trigger' }, + ], + injectDiscriminantOnEncode: true, +}; + +const AUTOMATION_RUN_LIFECYCLE_UNION: UnionConfig = { + name: 'AutomationRunLifecycle', + discriminantField: 'status', + variants: [ + { caseName: 'pending', structName: 'AutomationPendingRunLifecycle', discriminantValue: 'pending' }, + { caseName: 'running', structName: 'AutomationRunningRunLifecycle', discriminantValue: 'running' }, + { caseName: 'blocked', structName: 'AutomationBlockedRunLifecycle', discriminantValue: 'blocked' }, + { caseName: 'completed', structName: 'AutomationCompletedRunLifecycle', discriminantValue: 'completed' }, + { caseName: 'failed', structName: 'AutomationFailedRunLifecycle', discriminantValue: 'failed' }, + { caseName: 'cancelled', structName: 'AutomationCancelledRunLifecycle', discriminantValue: 'cancelled' }, + ], + injectDiscriminantOnEncode: true, +}; + function generateStateFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; @@ -1190,6 +1279,16 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); + lines.push(''); + lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_LIFECYCLE_UNION)); + lines.push(''); lines.push(generateToolResultContentUnion()); lines.push(''); lines.push(generateSnapshotState()); @@ -1287,6 +1386,17 @@ const ACTION_VARIANTS: { type: string; caseName: string; tsInterface: string }[] { type: 'terminal/commandExecuted', caseName: 'terminalCommandExecuted', tsInterface: 'TerminalCommandExecutedAction' }, { type: 'terminal/commandFinished', caseName: 'terminalCommandFinished', tsInterface: 'TerminalCommandFinishedAction' }, { type: 'resourceWatch/changed', caseName: 'resourceWatchChanged', tsInterface: 'ResourceWatchChangedAction' }, + { type: 'automation/definitionChanged', caseName: 'automationDefinitionChanged', tsInterface: 'AutomationDefinitionChangedAction' }, + { type: 'automation/runSummarySet', caseName: 'automationRunSummarySet', tsInterface: 'AutomationRunSummarySetAction' }, + { type: 'automation/runSummaryRemoved', caseName: 'automationRunSummaryRemoved', tsInterface: 'AutomationRunSummaryRemovedAction' }, + { type: 'automation/runsLoaded', caseName: 'automationRunsLoaded', tsInterface: 'AutomationRunsLoadedAction' }, + { type: 'automationRun/lifecycleChanged', caseName: 'automationRunLifecycleChanged', tsInterface: 'AutomationRunLifecycleChangedAction' }, + { type: 'automationRun/sessionSet', caseName: 'automationRunSessionSet', tsInterface: 'AutomationRunSessionSetAction' }, + { type: 'automationRun/sessionRemoved', caseName: 'automationRunSessionRemoved', tsInterface: 'AutomationRunSessionRemovedAction' }, + { type: 'automationRun/primarySessionChanged', caseName: 'automationRunPrimarySessionChanged', tsInterface: 'AutomationRunPrimarySessionChangedAction' }, + { type: 'automationRun/artifactSet', caseName: 'automationRunArtifactSet', tsInterface: 'AutomationRunArtifactSetAction' }, + { type: 'automationRun/artifactRemoved', caseName: 'automationRunArtifactRemoved', tsInterface: 'AutomationRunArtifactRemovedAction' }, + { type: 'automationRun/cancelRequested', caseName: 'automationRunCancelRequested', tsInterface: 'AutomationRunCancelRequestedAction' }, ]; /** Merged struct for the approved/denied tool call confirmed action */ @@ -1456,7 +1566,11 @@ function generateActionsFile(project: Project): string { const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode']; const COMMAND_STRUCTS = [ - 'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'Implementation', + 'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'AutomationCapabilities', + 'AutomationExecutionCapabilities', 'AutomationCreateCapability', + 'AutomationScheduleCapabilities', 'AutomationCronScheduleCapability', + 'AutomationRunCancellationCapability', 'AutomationSchedulePreviewCapability', + 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', @@ -1483,6 +1597,12 @@ const COMMAND_STRUCTS = [ 'CompletionsParams', 'CompletionItem', 'CompletionsResult', 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', + 'ListAutomationsParams', 'ListAutomationsResult', + 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', + 'CreateAutomationParams', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', + 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', + 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1648,7 +1768,9 @@ public struct ChangesetOperationRangeTarget: Codable, Sendable { const NOTIFICATION_ENUMS = ['AuthRequiredReason']; const NOTIFICATION_STRUCTS = [ - 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', 'ProgressParams', 'AuthRequiredParams', + 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'AutomationAddedParams', 'AutomationRemovedParams', 'AutomationSummaryChangedParams', + 'ProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', 'OtlpExportTracesParams', 'OtlpExportMetricsParams', ]; @@ -2136,6 +2258,11 @@ function checkExhaustiveness(project: Project): void { 'AhpErrorCodeWithData', // type-level alias; not a Swift type 'JsonRpcErrorCode', // type-level alias over JsonRpcErrorCodes const enum 'ReconnectResult', // RECONNECT_RESULT_UNION discriminated union + 'SessionOrigin', // SESSION_ORIGIN_UNION discriminated union + 'AutomationSchedule', // AUTOMATION_SCHEDULE_UNION discriminated union + 'AutomationTrigger', // AUTOMATION_TRIGGER_UNION discriminated union + 'AutomationRunCause', // AUTOMATION_RUN_CAUSE_UNION discriminated union + 'AutomationRunLifecycle', // AUTOMATION_RUN_LIFECYCLE_UNION discriminated union 'ForkChatSource', // generateFixedChatSourceBranchSwift() 'SideChatSource', // generateFixedChatSourceBranchSwift() 'ChangesetOperationTarget', // TS discriminated union; consumers should add a Swift case-iterable enum diff --git a/types/action-origin.generated.ts b/types/action-origin.generated.ts index 92cffe482..e5f806d68 100644 --- a/types/action-origin.generated.ts +++ b/types/action-origin.generated.ts @@ -88,6 +88,17 @@ import type { TerminalCommandExecutedAction, TerminalCommandFinishedAction, ResourceWatchChangedAction, + AutomationDefinitionChangedAction, + AutomationRunSummarySetAction, + AutomationRunSummaryRemovedAction, + AutomationRunsLoadedAction, + AutomationRunLifecycleChangedAction, + AutomationRunSessionSetAction, + AutomationRunSessionRemovedAction, + AutomationRunPrimarySessionChangedAction, + AutomationRunArtifactSetAction, + AutomationRunArtifactRemovedAction, + AutomationRunCancelRequestedAction, } from './actions.js'; import { ActionType } from './actions.js'; @@ -350,6 +361,53 @@ export type ServerResourceWatchAction = | ResourceWatchChangedAction ; +/** Union of all automation-scoped actions. */ +export type AutomationAction = + | AutomationDefinitionChangedAction + | AutomationRunSummarySetAction + | AutomationRunSummaryRemovedAction + | AutomationRunsLoadedAction +; + +/** Union of automation actions that clients may dispatch. */ +export type ClientAutomationAction = + never +; + +/** Union of automation actions that only the server may produce. */ +export type ServerAutomationAction = + | AutomationDefinitionChangedAction + | AutomationRunSummarySetAction + | AutomationRunSummaryRemovedAction + | AutomationRunsLoadedAction +; + +/** Union of all automation-run-scoped actions. */ +export type AutomationRunAction = + | AutomationRunLifecycleChangedAction + | AutomationRunSessionSetAction + | AutomationRunSessionRemovedAction + | AutomationRunPrimarySessionChangedAction + | AutomationRunArtifactSetAction + | AutomationRunArtifactRemovedAction + | AutomationRunCancelRequestedAction +; + +/** Union of automation-run actions that clients may dispatch. */ +export type ClientAutomationRunAction = + | AutomationRunCancelRequestedAction +; + +/** Union of automation-run actions that only the server may produce. */ +export type ServerAutomationRunAction = + | AutomationRunLifecycleChangedAction + | AutomationRunSessionSetAction + | AutomationRunSessionRemovedAction + | AutomationRunPrimarySessionChangedAction + | AutomationRunArtifactSetAction + | AutomationRunArtifactRemovedAction +; + // ─── Client-Dispatchable Map ───────────────────────────────────────────────── /** @@ -442,4 +500,15 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in StateAction['type']]: bool [ActionType.TerminalCommandExecuted]: false, [ActionType.TerminalCommandFinished]: false, [ActionType.ResourceWatchChanged]: false, + [ActionType.AutomationDefinitionChanged]: false, + [ActionType.AutomationRunSummarySet]: false, + [ActionType.AutomationRunSummaryRemoved]: false, + [ActionType.AutomationRunsLoaded]: false, + [ActionType.AutomationRunLifecycleChanged]: false, + [ActionType.AutomationRunSessionSet]: false, + [ActionType.AutomationRunSessionRemoved]: false, + [ActionType.AutomationRunPrimarySessionChanged]: false, + [ActionType.AutomationRunArtifactSet]: false, + [ActionType.AutomationRunArtifactRemoved]: false, + [ActionType.AutomationRunCancelRequested]: true, }; diff --git a/types/actions.ts b/types/actions.ts index a2469d66a..97ec41dc1 100644 --- a/types/actions.ts +++ b/types/actions.ts @@ -15,3 +15,5 @@ export * from './channels-terminal/actions.js'; export * from './channels-changeset/actions.js'; export * from './channels-annotations/actions.js'; export * from './channels-resource-watch/actions.js'; +export * from './channels-automation/actions.js'; +export * from './channels-automation-run/actions.js'; diff --git a/types/channels-automation-run/actions.ts b/types/channels-automation-run/actions.ts new file mode 100644 index 000000000..7614919cd --- /dev/null +++ b/types/channels-automation-run/actions.ts @@ -0,0 +1,54 @@ +/** + * Automation Run Channel Actions. + * + * @module channels-automation-run/actions + */ + +import { ActionType } from '../common/actions.js'; +import type { URI } from '../common/state.js'; +import type { AutomationRunArtifact, AutomationRunLifecycle, AutomationRunOperation } from './state.js'; + +/** @category Automation Run Actions */ +export interface AutomationRunLifecycleChangedAction { + type: ActionType.AutomationRunLifecycleChanged; + lifecycle: AutomationRunLifecycle; + operations: AutomationRunOperation[]; +} + +/** @category Automation Run Actions */ +export interface AutomationRunSessionSetAction { + type: ActionType.AutomationRunSessionSet; + session: URI; +} + +/** @category Automation Run Actions */ +export interface AutomationRunSessionRemovedAction { + type: ActionType.AutomationRunSessionRemoved; + session: URI; +} + +/** @category Automation Run Actions */ +export interface AutomationRunPrimarySessionChangedAction { + type: ActionType.AutomationRunPrimarySessionChanged; + primarySession?: URI; +} + +/** @category Automation Run Actions */ +export interface AutomationRunArtifactSetAction { + type: ActionType.AutomationRunArtifactSet; + artifact: AutomationRunArtifact; +} + +/** @category Automation Run Actions */ +export interface AutomationRunArtifactRemovedAction { + type: ActionType.AutomationRunArtifactRemoved; + artifactId: string; +} + +/** + * @category Automation Run Actions + * @clientDispatchable + */ +export interface AutomationRunCancelRequestedAction { + type: ActionType.AutomationRunCancelRequested; +} diff --git a/types/channels-automation-run/reducer.ts b/types/channels-automation-run/reducer.ts new file mode 100644 index 000000000..0b71f612f --- /dev/null +++ b/types/channels-automation-run/reducer.ts @@ -0,0 +1,75 @@ +/** + * Automation Run Channel Reducer. + * + * @module channels-automation-run/reducer + */ + +import type { AutomationRunAction } from '../action-origin.generated.js'; +import { ActionType } from '../common/actions.js'; +import { softAssertNever } from '../common/reducer-helpers.js'; +import type { AutomationRunArtifact, AutomationRunState } from './state.js'; + +/** Pure reducer for automation-run state. */ +export function automationRunReducer(state: AutomationRunState, action: AutomationRunAction, log?: (msg: string) => void): AutomationRunState { + switch (action.type) { + case ActionType.AutomationRunLifecycleChanged: + return { ...state, lifecycle: action.lifecycle, operations: action.operations }; + + case ActionType.AutomationRunSessionSet: + if (state.sessions.includes(action.session)) { + return state; + } + return { ...state, sessions: [...state.sessions, action.session] }; + + case ActionType.AutomationRunSessionRemoved: { + const index = state.sessions.indexOf(action.session); + if (index < 0) { + return state; + } + const sessions = state.sessions.slice(); + sessions.splice(index, 1); + const next: AutomationRunState = { ...state, sessions }; + if (state.primarySession === action.session) { + delete next.primarySession; + } + return next; + } + + case ActionType.AutomationRunPrimarySessionChanged: { + const next: AutomationRunState = { ...state }; + if (action.primarySession === undefined) { + delete next.primarySession; + } else { + next.primarySession = action.primarySession; + } + return next; + } + + case ActionType.AutomationRunArtifactSet: { + const index = state.artifacts.findIndex(artifact => artifact.id === action.artifact.id); + if (index < 0) { + return { ...state, artifacts: [...state.artifacts, action.artifact] }; + } + const artifacts: AutomationRunArtifact[] = state.artifacts.slice(); + artifacts[index] = action.artifact; + return { ...state, artifacts }; + } + + case ActionType.AutomationRunArtifactRemoved: { + const index = state.artifacts.findIndex(artifact => artifact.id === action.artifactId); + if (index < 0) { + return state; + } + const artifacts = state.artifacts.slice(); + artifacts.splice(index, 1); + return { ...state, artifacts }; + } + + case ActionType.AutomationRunCancelRequested: + return state; + + default: + softAssertNever(action, log); + return state; + } +} diff --git a/types/channels-automation-run/state.ts b/types/channels-automation-run/state.ts new file mode 100644 index 000000000..77a264379 --- /dev/null +++ b/types/channels-automation-run/state.ts @@ -0,0 +1,150 @@ +/** + * Automation Run Channel State Types. + * + * @module channels-automation-run/state + */ + +import type { ContentRef, ErrorInfo, URI, UsageInfo } from '../common/state.js'; + +/** @category Automation Run State */ +export const enum AutomationRunStatus { + Pending = 'pending', + Running = 'running', + Blocked = 'blocked', + Completed = 'completed', + Failed = 'failed', + Cancelled = 'cancelled', +} + +/** @category Automation Run State */ +export const enum AutomationRunBlockerKind { + UserInput = 'userInput', + ToolConfirmation = 'toolConfirmation', + Authentication = 'authentication', + ClientExecution = 'clientExecution', +} + +/** @category Automation Run State */ +export interface AutomationRunBlocker { + kind: AutomationRunBlockerKind; +} + +/** @category Automation Run State */ +export const enum AutomationRunCauseKind { + Manual = 'manual', + Trigger = 'trigger', +} + +/** @category Automation Run State */ +export interface AutomationManualRunCause { + kind: AutomationRunCauseKind.Manual; +} + +/** @category Automation Run State */ +export interface AutomationTriggeredRunCause { + kind: AutomationRunCauseKind.Trigger; + triggerId: string; + scheduledFor?: string; + catchUp?: boolean; + /** Host-defined event provenance containing no secrets. */ + event?: Record; +} + +/** @category Automation Run State */ +export type AutomationRunCause = + | AutomationManualRunCause + | AutomationTriggeredRunCause; + +/** @category Automation Run State */ +export interface AutomationPendingRunLifecycle { + status: AutomationRunStatus.Pending; + createdAt: string; +} + +/** @category Automation Run State */ +export interface AutomationRunningRunLifecycle { + status: AutomationRunStatus.Running; + createdAt: string; + startedAt: string; +} + +/** @category Automation Run State */ +export interface AutomationBlockedRunLifecycle { + status: AutomationRunStatus.Blocked; + createdAt: string; + startedAt: string; + blocker: AutomationRunBlocker; +} + +/** @category Automation Run State */ +export interface AutomationCompletedRunLifecycle { + status: AutomationRunStatus.Completed; + createdAt: string; + startedAt: string; + completedAt: string; + usage?: UsageInfo; +} + +/** @category Automation Run State */ +export interface AutomationFailedRunLifecycle { + status: AutomationRunStatus.Failed; + createdAt: string; + startedAt?: string; + completedAt: string; + error: ErrorInfo; +} + +/** @category Automation Run State */ +export interface AutomationCancelledRunLifecycle { + status: AutomationRunStatus.Cancelled; + createdAt: string; + startedAt?: string; + completedAt: string; +} + +/** @category Automation Run State */ +export type AutomationRunLifecycle = + | AutomationPendingRunLifecycle + | AutomationRunningRunLifecycle + | AutomationBlockedRunLifecycle + | AutomationCompletedRunLifecycle + | AutomationFailedRunLifecycle + | AutomationCancelledRunLifecycle; + +/** @category Automation Run State */ +export const enum AutomationRunOperation { + Cancel = 'cancel', +} + +/** @category Automation Run State */ +export interface AutomationRunArtifact extends ContentRef { + id: string; + label: string; + _meta?: Record; +} + +/** @category Automation Run State */ +export interface AutomationRunSummary { + resource: URI; + automation: URI; + cause: AutomationRunCause; + lifecycle: AutomationRunLifecycle; + primarySession?: URI; + sessionCount: number; + artifactCount?: number; + operations: AutomationRunOperation[]; + _meta?: Record; +} + +/** @category Automation Run State */ +export interface AutomationRunState { + resource: URI; + automation: URI; + cause: AutomationRunCause; + lifecycle: AutomationRunLifecycle; + sessions: URI[]; + primarySession?: URI; + artifacts: AutomationRunArtifact[]; + operations: AutomationRunOperation[]; + _meta?: Record; +} diff --git a/types/channels-automation/actions.ts b/types/channels-automation/actions.ts new file mode 100644 index 000000000..011b502dd --- /dev/null +++ b/types/channels-automation/actions.ts @@ -0,0 +1,39 @@ +/** + * Automation Channel Actions. + * + * @module channels-automation/actions + */ + +import { ActionType } from '../common/actions.js'; +import type { URI } from '../common/state.js'; +import type { AutomationDefinition } from './state.js'; +import type { AutomationRunSummary } from '../channels-automation-run/state.js'; + +/** @category Automation Actions */ +export interface AutomationDefinitionChangedAction { + type: ActionType.AutomationDefinitionChanged; + definition: AutomationDefinition; + revision: number; + modifiedAt: string; + nextRunAt?: string; +} + +/** @category Automation Actions */ +export interface AutomationRunSummarySetAction { + type: ActionType.AutomationRunSummarySet; + run: AutomationRunSummary; +} + +/** @category Automation Actions */ +export interface AutomationRunSummaryRemovedAction { + type: ActionType.AutomationRunSummaryRemoved; + run: URI; +} + +/** @category Automation Actions */ +export interface AutomationRunsLoadedAction { + type: ActionType.AutomationRunsLoaded; + runs: AutomationRunSummary[]; + nextCursor?: string; +} + diff --git a/types/channels-automation/commands.ts b/types/channels-automation/commands.ts new file mode 100644 index 000000000..f330f3247 --- /dev/null +++ b/types/channels-automation/commands.ts @@ -0,0 +1,142 @@ +/** + * Automation Channel Commands. + * + * @module channels-automation/commands + */ + +import type { BaseParams, PaginatedParams, PaginatedResult } from '../common/commands.js'; +import type { URI } from '../common/state.js'; +import type { + AutomationDefinition, + AutomationSchedule, + AutomationSessionTemplate, + AutomationSummary, + AutomationTrigger, + AutomationTriggerDefinition, +} from './state.js'; +import type { Message } from '../channels-chat/state.js'; + +/** + * @category Commands + * @method listAutomations + * @direction Client → Server + * @messageType Request + */ +export interface ListAutomationsParams extends BaseParams, PaginatedParams { + channel: 'ahp-root://'; + enabled?: boolean; +} + +/** @category Commands */ +export interface ListAutomationsResult extends PaginatedResult { + items: AutomationSummary[]; +} + +/** + * @category Commands + * @method listAutomationTriggerDefinitions + * @direction Client → Server + * @messageType Request + */ +export interface ListAutomationTriggerDefinitionsParams extends BaseParams { + channel: 'ahp-root://'; + provider?: string; + workingDirectories?: URI[]; + sessionConfig?: Record; +} + +/** @category Commands */ +export interface ListAutomationTriggerDefinitionsResult { + items: AutomationTriggerDefinition[]; +} + +/** + * @category Commands + * @method createAutomation + * @direction Client → Server + * @messageType Request + */ +export interface CreateAutomationParams extends BaseParams { + channel: URI; + definition: AutomationDefinition; + import?: { + source: string; + batchId: string; + itemId: string; + }; +} + +/** @category Commands */ +export interface AutomationDefinitionPatch { + title?: string; + message?: Message; + session?: AutomationSessionTemplate; + enabled?: boolean; + triggers?: AutomationTrigger[]; + _meta?: Record; +} + +/** + * @category Commands + * @method updateAutomation + * @direction Client → Server + * @messageType Request + */ +export interface UpdateAutomationParams extends BaseParams { + channel: URI; + expectedRevision: number; + changes: AutomationDefinitionPatch; +} + +/** + * @category Commands + * @method disposeAutomation + * @direction Client → Server + * @messageType Request + */ +export interface DisposeAutomationParams extends BaseParams {} + +/** + * @category Commands + * @method runAutomation + * @direction Client → Server + * @messageType Request + */ +export interface RunAutomationParams extends BaseParams { + requestId: string; +} + +/** @category Commands */ +export interface RunAutomationResult { + run: URI; +} + +/** + * @category Commands + * @method fetchAutomationRuns + * @direction Client → Server + * @messageType Request + */ +export interface FetchAutomationRunsParams extends BaseParams { + cursor?: string; +} + +/** @category Commands */ +export interface FetchAutomationRunsResult {} + +/** + * @category Commands + * @method previewAutomationSchedule + * @direction Client → Server + * @messageType Request + */ +export interface PreviewAutomationScheduleParams extends BaseParams { + channel: 'ahp-root://'; + schedule: AutomationSchedule; + count?: number; +} + +/** @category Commands */ +export interface PreviewAutomationScheduleResult { + items: string[]; +} diff --git a/types/channels-automation/reducer.ts b/types/channels-automation/reducer.ts new file mode 100644 index 000000000..47f81e6e5 --- /dev/null +++ b/types/channels-automation/reducer.ts @@ -0,0 +1,73 @@ +/** + * Automation Channel Reducer. + * + * @module channels-automation/reducer + */ + +import type { AutomationAction } from '../action-origin.generated.js'; +import { ActionType } from '../common/actions.js'; +import { softAssertNever } from '../common/reducer-helpers.js'; +import type { AutomationRunSummary } from '../channels-automation-run/state.js'; +import type { AutomationState } from './state.js'; + +/** Pure reducer for automation state. */ +export function automationReducer(state: AutomationState, action: AutomationAction, log?: (msg: string) => void): AutomationState { + switch (action.type) { + case ActionType.AutomationDefinitionChanged: { + const next: AutomationState = { + ...state, + definition: action.definition, + revision: action.revision, + modifiedAt: action.modifiedAt, + }; + if (action.nextRunAt === undefined) { + delete next.nextRunAt; + } else { + next.nextRunAt = action.nextRunAt; + } + return next; + } + + case ActionType.AutomationRunSummarySet: { + const index = state.runs.findIndex(run => run.resource === action.run.resource); + if (index < 0) { + return { ...state, runs: [action.run, ...state.runs] }; + } + const runs: AutomationRunSummary[] = state.runs.slice(); + runs[index] = action.run; + return { ...state, runs }; + } + + case ActionType.AutomationRunSummaryRemoved: { + const index = state.runs.findIndex(run => run.resource === action.run); + if (index < 0) { + return state; + } + const runs = state.runs.slice(); + runs.splice(index, 1); + return { ...state, runs }; + } + + case ActionType.AutomationRunsLoaded: { + const known = new Set(state.runs.map(run => run.resource)); + const runs = [...state.runs, ...action.runs.filter(run => { + if (known.has(run.resource)) { + return false; + } + known.add(run.resource); + return true; + })]; + const next: AutomationState = { ...state, runs }; + if (action.nextCursor === undefined) { + delete next.runsNextCursor; + } else { + next.runsNextCursor = action.nextCursor; + } + return next; + } + + default: + softAssertNever(action, log); + return state; + } +} diff --git a/types/channels-automation/state.ts b/types/channels-automation/state.ts new file mode 100644 index 000000000..cbf9a8d4e --- /dev/null +++ b/types/channels-automation/state.ts @@ -0,0 +1,199 @@ +/** + * Automation Channel State Types. + * + * @module channels-automation/state + */ + +import type { Message } from '../channels-chat/state.js'; +import type { ConfigSchema, URI } from '../common/state.js'; +import type { AutomationRunSummary } from '../channels-automation-run/state.js'; + +/** @category Automation State */ +export const enum AutomationOperation { + Update = 'update', + Dispose = 'dispose', + Run = 'run', +} + +/** @category Automation State */ +export const enum AutomationExecutionLifetime { + HostLifetime = 'hostLifetime', + Managed = 'managed', +} + +/** @category Automation State */ +export const enum AutomationScheduleKind { + Hourly = 'hourly', + Daily = 'daily', + Weekly = 'weekly', + Cron = 'cron', +} + +/** @category Automation State */ +export const enum AutomationWeekday { + Monday = 'monday', + Tuesday = 'tuesday', + Wednesday = 'wednesday', + Thursday = 'thursday', + Friday = 'friday', + Saturday = 'saturday', + Sunday = 'sunday', +} + +/** @category Automation State */ +export interface AutomationLocalTime { + hour: number; + minute: number; +} + +/** @category Automation State */ +export interface AutomationHourlySchedule { + kind: AutomationScheduleKind.Hourly; +} + +/** @category Automation State */ +export interface AutomationDailySchedule { + kind: AutomationScheduleKind.Daily; + time: AutomationLocalTime; + /** IANA time-zone identifier. */ + timeZone: string; +} + +/** @category Automation State */ +export interface AutomationWeeklySchedule { + kind: AutomationScheduleKind.Weekly; + weekday: AutomationWeekday; + time: AutomationLocalTime; + /** IANA time-zone identifier. */ + timeZone: string; +} + +/** @category Automation State */ +export interface AutomationCronSchedule { + kind: AutomationScheduleKind.Cron; + /** Standard five-field Unix cron expression. */ + expression: string; + /** IANA time-zone identifier. */ + timeZone: string; +} + +/** @category Automation State */ +export type AutomationSchedule = + | AutomationHourlySchedule + | AutomationDailySchedule + | AutomationWeeklySchedule + | AutomationCronSchedule; + +/** @category Automation State */ +export const enum AutomationMisfirePolicy { + Skip = 'skip', + RunOnce = 'runOnce', +} + +/** @category Automation State */ +export const enum AutomationTriggerKind { + Schedule = 'schedule', + Event = 'event', +} + +/** @category Automation State */ +export interface AutomationScheduleTrigger { + /** Stable within the automation definition. */ + id: string; + kind: AutomationTriggerKind.Schedule; + schedule: AutomationSchedule; + misfirePolicy?: AutomationMisfirePolicy; +} + +/** @category Automation State */ +export interface AutomationEventTrigger { + /** Stable within the automation definition. */ + id: string; + kind: AutomationTriggerKind.Event; + /** Stable host-defined trigger type. */ + type: string; + /** Selected event actions. */ + events: string[]; + /** Schema-defined values. Unknown entries must survive round-trips. */ + config?: Record; +} + +/** @category Automation State */ +export type AutomationTrigger = + | AutomationScheduleTrigger + | AutomationEventTrigger; + +/** @category Automation State */ +export interface AutomationTriggerEventDefinition { + id: string; + title: string; + description?: string; +} + +/** @category Automation State */ +export interface AutomationTriggerDefinition { + type: string; + title: string; + description?: string; + events: AutomationTriggerEventDefinition[]; + configSchema?: ConfigSchema; +} + +/** @category Automation State */ +export interface AutomationSessionTemplate { + provider?: string; + /** Absence means a workspace-less session. */ + workingDirectories?: URI[]; + /** Values resolved through `resolveSessionConfig`. */ + config?: Record; +} + +/** @category Automation State */ +export interface AutomationDefinition { + title: string; + /** Initial user message sent to each new session. */ + message: Message; + session: AutomationSessionTemplate; + /** Controls automatic triggers; manual runs remain permitted. */ + enabled: boolean; + /** Empty means manual-only. */ + triggers: AutomationTrigger[]; + _meta?: Record; +} + +/** @category Automation State */ +export interface AutomationRuntimeState { + workingDirectories?: URI[]; + _meta?: Record; +} + +/** @category Automation State */ +export interface AutomationSummary { + resource: URI; + title: string; + enabled: boolean; + triggerCount: number; + nextRunAt?: string; + lastRun?: AutomationRunSummary; + revision: number; + operations: AutomationOperation[]; + createdAt: string; + modifiedAt: string; + _meta?: Record; +} + +/** @category Automation State */ +export interface AutomationState { + resource: URI; + definition: AutomationDefinition; + revision: number; + nextRunAt?: string; + /** Newest-first retained run summaries. */ + runs: AutomationRunSummary[]; + runsNextCursor?: string; + runtime?: AutomationRuntimeState; + operations: AutomationOperation[]; + createdAt: string; + modifiedAt: string; + _meta?: Record; +} diff --git a/types/channels-root/notifications.ts b/types/channels-root/notifications.ts index eb74fedc5..482a391b3 100644 --- a/types/channels-root/notifications.ts +++ b/types/channels-root/notifications.ts @@ -7,6 +7,7 @@ import type { URI } from '../common/state.js'; import type { SessionSummary } from '../channels-session/state.js'; +import type { AutomationSummary } from '../channels-automation/state.js'; // ─── root/sessionAdded ─────────────────────────────────────────────────────── @@ -143,6 +144,45 @@ export interface SessionSummaryChangedParams { changes: Partial; } +// ─── root/automationAdded ──────────────────────────────────────────────────── + +/** + * @category Protocol Notifications + * @method root/automationAdded + * @direction Server → Client + * @messageType Notification + */ +export interface AutomationAddedParams { + channel: URI; + summary: AutomationSummary; +} + +// ─── root/automationRemoved ────────────────────────────────────────────────── + +/** + * @category Protocol Notifications + * @method root/automationRemoved + * @direction Server → Client + * @messageType Notification + */ +export interface AutomationRemovedParams { + channel: URI; + automation: URI; +} + +// ─── root/automationSummaryChanged ─────────────────────────────────────────── + +/** + * @category Protocol Notifications + * @method root/automationSummaryChanged + * @direction Server → Client + * @messageType Notification + */ +export interface AutomationSummaryChangedParams { + channel: URI; + summary: AutomationSummary; +} + // ─── progress ──────────────────────────────────────────────────────────────── /** diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index e26fecfc7..8055ce2c7 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -59,6 +59,21 @@ export const enum SessionStatus { IsArchived = 1 << 6, } +/** @category Session State */ +export const enum SessionOriginKind { + Automation = 'automation', +} + +/** @category Session State */ +export interface AutomationSessionOrigin { + kind: SessionOriginKind.Automation; + automation: URI; + run: URI; +} + +/** @category Session State */ +export type SessionOrigin = AutomationSessionOrigin; + /** * Metadata shared between the full {@link SessionState} (delivered when a * client subscribes to a session's URI) and the lightweight @@ -81,6 +96,8 @@ export interface SessionMetadata { status: SessionStatus; /** Human-readable description of what the session is currently doing */ activity?: string; + /** Durable origin of this session, when another AHP resource created it. */ + origin?: SessionOrigin; /** Server-owned project for this session */ project?: ProjectInfo; /** diff --git a/types/commands.ts b/types/commands.ts index c77dd9492..700bbfa0b 100644 --- a/types/commands.ts +++ b/types/commands.ts @@ -14,3 +14,4 @@ export * from './channels-chat/commands.js'; export * from './channels-terminal/commands.js'; export * from './channels-changeset/commands.js'; export * from './channels-resource-watch/commands.js'; +export * from './channels-automation/commands.js'; diff --git a/types/common/actions.ts b/types/common/actions.ts index d07164129..4f66e7e2a 100644 --- a/types/common/actions.ts +++ b/types/common/actions.ts @@ -113,6 +113,21 @@ import type { import type { ResourceWatchChangedAction, } from '../channels-resource-watch/actions.js'; +import type { + AutomationDefinitionChangedAction, + AutomationRunSummarySetAction, + AutomationRunSummaryRemovedAction, + AutomationRunsLoadedAction, +} from '../channels-automation/actions.js'; +import type { + AutomationRunLifecycleChangedAction, + AutomationRunSessionSetAction, + AutomationRunSessionRemovedAction, + AutomationRunPrimarySessionChangedAction, + AutomationRunArtifactSetAction, + AutomationRunArtifactRemovedAction, + AutomationRunCancelRequestedAction, +} from '../channels-automation-run/actions.js'; // ─── Action Type Enum ──────────────────────────────────────────────────────── @@ -207,6 +222,17 @@ export const enum ActionType { TerminalCommandExecuted = 'terminal/commandExecuted', TerminalCommandFinished = 'terminal/commandFinished', ResourceWatchChanged = 'resourceWatch/changed', + AutomationDefinitionChanged = 'automation/definitionChanged', + AutomationRunSummarySet = 'automation/runSummarySet', + AutomationRunSummaryRemoved = 'automation/runSummaryRemoved', + AutomationRunsLoaded = 'automation/runsLoaded', + AutomationRunLifecycleChanged = 'automationRun/lifecycleChanged', + AutomationRunSessionSet = 'automationRun/sessionSet', + AutomationRunSessionRemoved = 'automationRun/sessionRemoved', + AutomationRunPrimarySessionChanged = 'automationRun/primarySessionChanged', + AutomationRunArtifactSet = 'automationRun/artifactSet', + AutomationRunArtifactRemoved = 'automationRun/artifactRemoved', + AutomationRunCancelRequested = 'automationRun/cancelRequested', } // ─── Action Envelope ───────────────────────────────────────────────────────── @@ -327,4 +353,15 @@ export type StateAction = | TerminalCommandDetectionAvailableAction | TerminalCommandExecutedAction | TerminalCommandFinishedAction - | ResourceWatchChangedAction; + | ResourceWatchChangedAction + | AutomationDefinitionChangedAction + | AutomationRunSummarySetAction + | AutomationRunSummaryRemovedAction + | AutomationRunsLoadedAction + | AutomationRunLifecycleChangedAction + | AutomationRunSessionSetAction + | AutomationRunSessionRemovedAction + | AutomationRunPrimarySessionChangedAction + | AutomationRunArtifactSetAction + | AutomationRunArtifactRemovedAction + | AutomationRunCancelRequestedAction; diff --git a/types/common/commands.ts b/types/common/commands.ts index 38ee9bd6e..d982dec40 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -10,6 +10,7 @@ import type { URI, Snapshot } from './state.js'; import type { ActionEnvelope, StateAction } from './actions.js'; import type { TelemetryCapabilities } from '../channels-otlp/state.js'; +import type { AutomationExecutionLifetime, AutomationScheduleKind } from '../channels-automation/state.js'; // ─── BaseParams ────────────────────────────────────────────────────────────── @@ -266,8 +267,46 @@ export interface InitializeResult { * @see {@link /specification/telemetry-channel | Telemetry Channel} */ telemetry?: TelemetryCapabilities; + /** Host automation support. Absence means unsupported. */ + automations?: AutomationCapabilities; } +/** @category Commands */ +export interface AutomationCapabilities { + execution: AutomationExecutionCapabilities; + create?: AutomationCreateCapability; + schedules?: AutomationScheduleCapabilities; + runCancellation?: AutomationRunCancellationCapability; + schedulePreview?: AutomationSchedulePreviewCapability; + runHistoryLimit?: number; +} + +/** @category Commands */ +export interface AutomationExecutionCapabilities { + lifetime: AutomationExecutionLifetime; +} + +/** @category Commands */ +export interface AutomationCreateCapability {} + +/** @category Commands */ +export interface AutomationScheduleCapabilities { + kinds: AutomationScheduleKind[]; + cron?: AutomationCronScheduleCapability; +} + +/** @category Commands */ +export interface AutomationCronScheduleCapability { + dialect: 'unix5'; + minIntervalMinutes?: number; +} + +/** @category Commands */ +export interface AutomationRunCancellationCapability {} + +/** @category Commands */ +export interface AutomationSchedulePreviewCapability {} + // ─── ping ──────────────────────────────────────────────────────────────────── /** diff --git a/types/common/messages.ts b/types/common/messages.ts index e248b9e26..27328c737 100644 --- a/types/common/messages.ts +++ b/types/common/messages.ts @@ -70,6 +70,21 @@ import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult, } from '../channels-changeset/commands.js'; +import type { + ListAutomationsParams, + ListAutomationsResult, + ListAutomationTriggerDefinitionsParams, + ListAutomationTriggerDefinitionsResult, + CreateAutomationParams, + UpdateAutomationParams, + DisposeAutomationParams, + RunAutomationParams, + RunAutomationResult, + FetchAutomationRunsParams, + FetchAutomationRunsResult, + PreviewAutomationScheduleParams, + PreviewAutomationScheduleResult, +} from '../channels-automation/commands.js'; import type { ActionEnvelope } from './actions.js'; import type { @@ -77,6 +92,9 @@ import type { SessionRemovedParams, SessionSummaryChangedParams, ProgressParams, + AutomationAddedParams, + AutomationRemovedParams, + AutomationSummaryChangedParams, } from '../channels-root/notifications.js'; import type { AuthRequiredParams } from './notifications.js'; import type { @@ -174,6 +192,14 @@ export interface CommandMap { 'sessionConfigCompletions': { params: SessionConfigCompletionsParams; result: SessionConfigCompletionsResult }; 'completions': { params: CompletionsParams; result: CompletionsResult }; 'invokeChangesetOperation': { params: InvokeChangesetOperationParams; result: InvokeChangesetOperationResult }; + 'listAutomations': { params: ListAutomationsParams; result: ListAutomationsResult }; + 'listAutomationTriggerDefinitions': { params: ListAutomationTriggerDefinitionsParams; result: ListAutomationTriggerDefinitionsResult }; + 'createAutomation': { params: CreateAutomationParams; result: null }; + 'updateAutomation': { params: UpdateAutomationParams; result: null }; + 'disposeAutomation': { params: DisposeAutomationParams; result: null }; + 'runAutomation': { params: RunAutomationParams; result: RunAutomationResult }; + 'fetchAutomationRuns': { params: FetchAutomationRunsParams; result: FetchAutomationRunsResult }; + 'previewAutomationSchedule': { params: PreviewAutomationScheduleParams; result: PreviewAutomationScheduleResult }; } /** @@ -232,6 +258,9 @@ export interface ServerNotificationMap { 'root/sessionAdded': { params: SessionAddedParams }; 'root/sessionRemoved': { params: SessionRemovedParams }; 'root/sessionSummaryChanged': { params: SessionSummaryChangedParams }; + 'root/automationAdded': { params: AutomationAddedParams }; + 'root/automationRemoved': { params: AutomationRemovedParams }; + 'root/automationSummaryChanged': { params: AutomationSummaryChangedParams }; 'root/progress': { params: ProgressParams }; 'auth/required': { params: AuthRequiredParams }; 'otlp/exportLogs': { params: OtlpExportLogsParams }; diff --git a/types/common/state.ts b/types/common/state.ts index be6c2e16d..b56048c6a 100644 --- a/types/common/state.ts +++ b/types/common/state.ts @@ -14,6 +14,8 @@ import type { ChangesetState } from '../channels-changeset/state.js'; import type { ResourceWatchState } from '../channels-resource-watch/state.js'; import type { AnnotationsState } from '../channels-annotations/state.js'; import type { ChatState } from '../channels-chat/state.js'; +import type { AutomationState } from '../channels-automation/state.js'; +import type { AutomationRunState } from '../channels-automation-run/state.js'; // ─── Type Aliases ──────────────────────────────────────────────────────────── @@ -333,7 +335,7 @@ export interface Snapshot { /** The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`) */ resource: URI; /** The current state of the resource */ - state: RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState; + state: RootState | SessionState | TerminalState | ChangesetState | ResourceWatchState | AnnotationsState | ChatState | AutomationState | AutomationRunState; /** The `serverSeq` at which this snapshot was taken. Subsequent actions will have `serverSeq > fromSeq`. */ fromSeq: number; } diff --git a/types/index.ts b/types/index.ts index a9b0f0f3e..048e196fe 100644 --- a/types/index.ts +++ b/types/index.ts @@ -29,5 +29,7 @@ export { changesetReducer, annotationsReducer, resourceWatchReducer, + automationReducer, + automationRunReducer, isClientDispatchable, } from './reducers.js'; diff --git a/types/messages.test.ts b/types/messages.test.ts index 591a9d7c4..7abe8b024 100644 --- a/types/messages.test.ts +++ b/types/messages.test.ts @@ -33,6 +33,8 @@ function readChannelSources(baseName: string): string { 'channels-changeset', 'channels-annotations', 'channels-resource-watch', + 'channels-automation', + 'channels-automation-run', ]; return dirs .map(dir => { diff --git a/types/reducers.test.ts b/types/reducers.test.ts index 0d04b7398..df9308485 100644 --- a/types/reducers.test.ts +++ b/types/reducers.test.ts @@ -25,11 +25,13 @@ import { changesetReducer, annotationsReducer, resourceWatchReducer, + automationReducer, + automationRunReducer, isClientDispatchable, } from './reducers.js'; import { IS_CLIENT_DISPATCHABLE } from './action-origin.generated.js'; import { ActionType } from './actions.js'; -import type { RootState, SessionState, ChatState, TerminalState, ChangesetState, AnnotationsState, ResourceWatchState } from './state.js'; +import type { RootState, SessionState, ChatState, TerminalState, ChangesetState, AnnotationsState, ResourceWatchState, AutomationState, AutomationRunState } from './state.js'; import { SessionStatus, TurnState, @@ -54,6 +56,8 @@ function readChannelSources(baseName: string): string { 'channels-changeset', 'channels-annotations', 'channels-resource-watch', + 'channels-automation', + 'channels-automation-run', ]; return dirs .map(dir => { @@ -69,11 +73,11 @@ function readChannelSources(baseName: string): string { // ─── Fixture Loading ───────────────────────────────────────────────────────── -type FixtureState = RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState | ResourceWatchState; +type FixtureState = RootState | SessionState | ChatState | TerminalState | ChangesetState | AnnotationsState | ResourceWatchState | AutomationState | AutomationRunState; interface Fixture { description: string; - reducer: 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch'; + reducer: 'root' | 'session' | 'chat' | 'terminal' | 'changeset' | 'annotations' | 'resourceWatch' | 'automation' | 'automationRun'; initial: FixtureState; actions: unknown[]; expected: FixtureState; @@ -140,6 +144,10 @@ describe('reducer fixtures', () => { state = annotationsReducer(state as AnnotationsState, action as any); } else if (fixture.reducer === 'resourceWatch') { state = resourceWatchReducer(state as ResourceWatchState, action as any); + } else if (fixture.reducer === 'automation') { + state = automationReducer(state as AutomationState, action as any); + } else if (fixture.reducer === 'automationRun') { + state = automationRunReducer(state as AutomationRunState, action as any); } else { state = sessionReducer(state as SessionState, action as any); } diff --git a/types/reducers.ts b/types/reducers.ts index d916ba3a9..f6cbdb31f 100644 --- a/types/reducers.ts +++ b/types/reducers.ts @@ -12,4 +12,6 @@ export { terminalReducer } from './channels-terminal/reducer.js'; export { changesetReducer } from './channels-changeset/reducer.js'; export { annotationsReducer } from './channels-annotations/reducer.js'; export { resourceWatchReducer } from './channels-resource-watch/reducer.js'; +export { automationReducer } from './channels-automation/reducer.js'; +export { automationRunReducer } from './channels-automation-run/reducer.js'; export { softAssertNever, isClientDispatchable } from './common/reducer-helpers.js'; diff --git a/types/state.ts b/types/state.ts index 8748e48d3..28b13bfd0 100644 --- a/types/state.ts +++ b/types/state.ts @@ -16,3 +16,5 @@ export * from './channels-changeset/state.js'; export * from './channels-annotations/state.js'; export * from './channels-otlp/state.js'; export * from './channels-resource-watch/state.js'; +export * from './channels-automation/state.js'; +export * from './channels-automation-run/state.js'; diff --git a/types/test-cases/reducers/263-automation-definitionchanged-clears-next-run.json b/types/test-cases/reducers/263-automation-definitionchanged-clears-next-run.json new file mode 100644 index 000000000..bf418e323 --- /dev/null +++ b/types/test-cases/reducers/263-automation-definitionchanged-clears-next-run.json @@ -0,0 +1,47 @@ +{ + "description": "automation definitionChanged replaces editable state and clears next run", + "reducer": "automation", + "initial": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "Old", + "message": { "text": "old", "origin": { "kind": "user" } }, + "session": {}, + "enabled": true, + "triggers": [] + }, + "revision": 1, + "nextRunAt": "2026-01-01T00:00:00Z", + "runs": [], + "operations": ["update", "dispose", "run"], + "createdAt": "2025-01-01T00:00:00Z", + "modifiedAt": "2025-01-01T00:00:00Z" + }, + "actions": [{ + "type": "automation/definitionChanged", + "definition": { + "title": "New", + "message": { "text": "new", "origin": { "kind": "user" } }, + "session": {}, + "enabled": false, + "triggers": [] + }, + "revision": 2, + "modifiedAt": "2025-01-02T00:00:00Z" + }], + "expected": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "New", + "message": { "text": "new", "origin": { "kind": "user" } }, + "session": {}, + "enabled": false, + "triggers": [] + }, + "revision": 2, + "runs": [], + "operations": ["update", "dispose", "run"], + "createdAt": "2025-01-01T00:00:00Z", + "modifiedAt": "2025-01-02T00:00:00Z" + } +} diff --git a/types/test-cases/reducers/264-automation-run-summary-lifecycle.json b/types/test-cases/reducers/264-automation-run-summary-lifecycle.json new file mode 100644 index 000000000..9267313d0 --- /dev/null +++ b/types/test-cases/reducers/264-automation-run-summary-lifecycle.json @@ -0,0 +1,105 @@ +{ + "description": "automation run summaries insert, replace, load, and remove", + "reducer": "automation", + "initial": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "A", + "message": { "text": "go", "origin": { "kind": "user" } }, + "session": {}, + "enabled": true, + "triggers": [] + }, + "revision": 1, + "runs": [], + "operations": ["run"], + "createdAt": "2025-01-01T00:00:00Z", + "modifiedAt": "2025-01-01T00:00:00Z" + }, + "actions": [ + { + "type": "automation/runSummarySet", + "run": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "2025-01-02T00:00:00Z" }, + "sessionCount": 0, + "operations": ["cancel"] + } + }, + { + "type": "automation/runSummarySet", + "run": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "running", + "createdAt": "2025-01-02T00:00:00Z", + "startedAt": "2025-01-02T00:00:01Z" + }, + "sessionCount": 1, + "operations": ["cancel"] + } + }, + { + "type": "automation/runsLoaded", + "runs": [ + { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "ignored" }, + "sessionCount": 0, + "operations": [] + }, + { + "resource": "ahp-automation-run:/r0", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "startedAt": "2025-01-01T00:00:01Z", + "completedAt": "2025-01-01T00:01:00Z" + }, + "sessionCount": 1, + "operations": [] + } + ], + "nextCursor": "older" + }, + { "type": "automation/runSummaryRemoved", "run": "ahp-automation-run:/r1" } + ], + "expected": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "A", + "message": { "text": "go", "origin": { "kind": "user" } }, + "session": {}, + "enabled": true, + "triggers": [] + }, + "revision": 1, + "runs": [ + { + "resource": "ahp-automation-run:/r0", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "startedAt": "2025-01-01T00:00:01Z", + "completedAt": "2025-01-01T00:01:00Z" + }, + "sessionCount": 1, + "operations": [] + } + ], + "runsNextCursor": "older", + "operations": ["run"], + "createdAt": "2025-01-01T00:00:00Z", + "modifiedAt": "2025-01-01T00:00:00Z" + } +} diff --git a/types/test-cases/reducers/265-automation-run-session-lifecycle.json b/types/test-cases/reducers/265-automation-run-session-lifecycle.json new file mode 100644 index 000000000..b732ef83e --- /dev/null +++ b/types/test-cases/reducers/265-automation-run-session-lifecycle.json @@ -0,0 +1,41 @@ +{ + "description": "automation run tracks lifecycle and linked sessions", + "reducer": "automationRun", + "initial": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "2025-01-01T00:00:00Z" }, + "sessions": [], + "artifacts": [], + "operations": ["cancel"] + }, + "actions": [ + { "type": "automationRun/sessionSet", "session": "ahp-session:/s1" }, + { "type": "automationRun/sessionSet", "session": "ahp-session:/s1" }, + { "type": "automationRun/primarySessionChanged", "primarySession": "ahp-session:/s1" }, + { + "type": "automationRun/lifecycleChanged", + "lifecycle": { + "status": "running", + "createdAt": "2025-01-01T00:00:00Z", + "startedAt": "2025-01-01T00:00:01Z" + }, + "operations": ["cancel"] + }, + { "type": "automationRun/sessionRemoved", "session": "ahp-session:/s1" } + ], + "expected": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "running", + "createdAt": "2025-01-01T00:00:00Z", + "startedAt": "2025-01-01T00:00:01Z" + }, + "sessions": [], + "artifacts": [], + "operations": ["cancel"] + } +} diff --git a/types/test-cases/reducers/266-automation-run-artifact-lifecycle.json b/types/test-cases/reducers/266-automation-run-artifact-lifecycle.json new file mode 100644 index 000000000..69ff566e0 --- /dev/null +++ b/types/test-cases/reducers/266-automation-run-artifact-lifecycle.json @@ -0,0 +1,44 @@ +{ + "description": "automation run artifacts insert, replace, and remove", + "reducer": "automationRun", + "initial": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "2025-01-01T00:00:00Z" }, + "sessions": [], + "artifacts": [], + "operations": ["cancel"] + }, + "actions": [ + { + "type": "automationRun/artifactSet", + "artifact": { + "id": "report", + "label": "Report", + "uri": "https://example.test/report", + "contentType": "text/markdown" + } + }, + { + "type": "automationRun/artifactSet", + "artifact": { + "id": "report", + "label": "Final report", + "uri": "https://example.test/report", + "contentType": "text/markdown" + } + }, + { "type": "automationRun/cancelRequested" }, + { "type": "automationRun/artifactRemoved", "artifactId": "report" } + ], + "expected": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "2025-01-01T00:00:00Z" }, + "sessions": [], + "artifacts": [], + "operations": ["cancel"] + } +} diff --git a/types/test-cases/reducers/267-automation-runsloaded-deduplicates-page.json b/types/test-cases/reducers/267-automation-runsloaded-deduplicates-page.json new file mode 100644 index 000000000..9d7b97442 --- /dev/null +++ b/types/test-cases/reducers/267-automation-runsloaded-deduplicates-page.json @@ -0,0 +1,62 @@ +{ + "description": "automation runsLoaded deduplicates repeated resources within one page", + "reducer": "automation", + "initial": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "A", + "message": { "text": "go", "origin": { "kind": "user" } }, + "session": {}, + "enabled": true, + "triggers": [] + }, + "revision": 1, + "runs": [], + "operations": ["run"], + "createdAt": "2025-01-01T00:00:00Z", + "modifiedAt": "2025-01-01T00:00:00Z" + }, + "actions": [{ + "type": "automation/runsLoaded", + "runs": [ + { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "2025-01-01T00:00:00Z" }, + "sessionCount": 0, + "operations": [] + }, + { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "2025-01-01T00:00:00Z" }, + "sessionCount": 0, + "operations": [] + } + ] + }], + "expected": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "A", + "message": { "text": "go", "origin": { "kind": "user" } }, + "session": {}, + "enabled": true, + "triggers": [] + }, + "revision": 1, + "runs": [{ + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { "status": "pending", "createdAt": "2025-01-01T00:00:00Z" }, + "sessionCount": 0, + "operations": [] + }], + "operations": ["run"], + "createdAt": "2025-01-01T00:00:00Z", + "modifiedAt": "2025-01-01T00:00:00Z" + } +} diff --git a/types/test-cases/reducers/268-automation-run-terminal-lifecycle-clears-operations.json b/types/test-cases/reducers/268-automation-run-terminal-lifecycle-clears-operations.json new file mode 100644 index 000000000..9e0a42887 --- /dev/null +++ b/types/test-cases/reducers/268-automation-run-terminal-lifecycle-clears-operations.json @@ -0,0 +1,43 @@ +{ + "description": "automation run terminal lifecycle replaces allowed operations", + "reducer": "automationRun", + "initial": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "running", + "createdAt": "2025-01-01T00:00:00Z", + "startedAt": "2025-01-01T00:00:01Z" + }, + "sessions": ["ahp-session:/s1"], + "primarySession": "ahp-session:/s1", + "artifacts": [], + "operations": ["cancel"] + }, + "actions": [{ + "type": "automationRun/lifecycleChanged", + "lifecycle": { + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "startedAt": "2025-01-01T00:00:01Z", + "completedAt": "2025-01-01T00:01:00Z" + }, + "operations": [] + }], + "expected": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "completed", + "createdAt": "2025-01-01T00:00:00Z", + "startedAt": "2025-01-01T00:00:01Z", + "completedAt": "2025-01-01T00:01:00Z" + }, + "sessions": ["ahp-session:/s1"], + "primarySession": "ahp-session:/s1", + "artifacts": [], + "operations": [] + } +} diff --git a/types/test-cases/round-trips/041-automation-snapshot.json b/types/test-cases/round-trips/041-automation-snapshot.json new file mode 100644 index 000000000..bc43e5e5c --- /dev/null +++ b/types/test-cases/round-trips/041-automation-snapshot.json @@ -0,0 +1,96 @@ +{ + "name": "automation-snapshot", + "group": "A", + "description": "An automation snapshot preserves schedule and event triggers plus run summaries.", + "type": "Snapshot", + "input": { + "resource": "ahp-automation:/a1", + "state": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "Triage", + "message": { "text": "Triage the issue", "origin": { "kind": "user" } }, + "session": { "provider": "copilot" }, + "enabled": true, + "triggers": [ + { + "id": "daily", + "kind": "schedule", + "schedule": { + "kind": "daily", + "time": { "hour": 9, "minute": 30 }, + "timeZone": "Europe/Berlin" + }, + "misfirePolicy": "runOnce" + }, + { + "id": "issue", + "kind": "event", + "type": "issues", + "events": ["opened"], + "config": { "query": "label:bug" } + } + ] + }, + "revision": 3, + "nextRunAt": "2026-08-06T07:30:00Z", + "runs": [{ + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "trigger", "triggerId": "issue", "event": { "issue": 42 } }, + "lifecycle": { "status": "pending", "createdAt": "2026-08-05T12:00:00Z" }, + "sessionCount": 0, + "operations": ["cancel"] + }], + "operations": ["update", "dispose", "run"], + "createdAt": "2026-08-01T00:00:00Z", + "modifiedAt": "2026-08-05T12:00:00Z" + }, + "fromSeq": 4 + }, + "acceptableOutputs": [{ + "resource": "ahp-automation:/a1", + "state": { + "resource": "ahp-automation:/a1", + "definition": { + "title": "Triage", + "message": { "text": "Triage the issue", "origin": { "kind": "user" } }, + "session": { "provider": "copilot" }, + "enabled": true, + "triggers": [ + { + "id": "daily", + "kind": "schedule", + "schedule": { + "kind": "daily", + "time": { "hour": 9, "minute": 30 }, + "timeZone": "Europe/Berlin" + }, + "misfirePolicy": "runOnce" + }, + { + "id": "issue", + "kind": "event", + "type": "issues", + "events": ["opened"], + "config": { "query": "label:bug" } + } + ] + }, + "revision": 3, + "nextRunAt": "2026-08-06T07:30:00Z", + "runs": [{ + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "trigger", "triggerId": "issue", "event": { "issue": 42 } }, + "lifecycle": { "status": "pending", "createdAt": "2026-08-05T12:00:00Z" }, + "sessionCount": 0, + "operations": ["cancel"] + }], + "operations": ["update", "dispose", "run"], + "createdAt": "2026-08-01T00:00:00Z", + "modifiedAt": "2026-08-05T12:00:00Z" + }, + "fromSeq": 4 + }] +} diff --git a/types/test-cases/round-trips/042-automation-run-snapshot.json b/types/test-cases/round-trips/042-automation-run-snapshot.json new file mode 100644 index 000000000..bc265421c --- /dev/null +++ b/types/test-cases/round-trips/042-automation-run-snapshot.json @@ -0,0 +1,54 @@ +{ + "name": "automation-run-snapshot", + "group": "A", + "description": "An automation-run snapshot preserves multiple session attempts and artifacts.", + "type": "Snapshot", + "input": { + "resource": "ahp-automation-run:/r1", + "state": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "completed", + "createdAt": "2026-08-05T12:00:00Z", + "startedAt": "2026-08-05T12:00:01Z", + "completedAt": "2026-08-05T12:03:00Z" + }, + "sessions": ["ahp-session:/old", "ahp-session:/retry"], + "primarySession": "ahp-session:/retry", + "artifacts": [{ + "id": "report", + "label": "Report", + "uri": "https://example.test/report", + "contentType": "text/markdown" + }], + "operations": [] + }, + "fromSeq": 9 + }, + "acceptableOutputs": [{ + "resource": "ahp-automation-run:/r1", + "state": { + "resource": "ahp-automation-run:/r1", + "automation": "ahp-automation:/a1", + "cause": { "kind": "manual" }, + "lifecycle": { + "status": "completed", + "createdAt": "2026-08-05T12:00:00Z", + "startedAt": "2026-08-05T12:00:01Z", + "completedAt": "2026-08-05T12:03:00Z" + }, + "sessions": ["ahp-session:/old", "ahp-session:/retry"], + "primarySession": "ahp-session:/retry", + "artifacts": [{ + "id": "report", + "label": "Report", + "uri": "https://example.test/report", + "contentType": "text/markdown" + }], + "operations": [] + }, + "fromSeq": 9 + }] +} diff --git a/types/test-cases/round-trips/043-automation-capabilities.json b/types/test-cases/round-trips/043-automation-capabilities.json new file mode 100644 index 000000000..fdc45808f --- /dev/null +++ b/types/test-cases/round-trips/043-automation-capabilities.json @@ -0,0 +1,38 @@ +{ + "name": "automation-capabilities", + "group": "A", + "description": "Automation capability markers preserve present empty objects and nested cron configuration.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.8.0", + "serverSeq": 1, + "snapshots": [], + "automations": { + "execution": { "lifetime": "managed" }, + "create": {}, + "schedules": { + "kinds": ["hourly", "daily", "weekly", "cron"], + "cron": { "dialect": "unix5", "minIntervalMinutes": 5 } + }, + "runCancellation": {}, + "schedulePreview": {}, + "runHistoryLimit": 50 + } + }, + "acceptableOutputs": [{ + "protocolVersion": "0.8.0", + "serverSeq": 1, + "snapshots": [], + "automations": { + "execution": { "lifetime": "managed" }, + "create": {}, + "schedules": { + "kinds": ["hourly", "daily", "weekly", "cron"], + "cron": { "dialect": "unix5", "minIntervalMinutes": 5 } + }, + "runCancellation": {}, + "schedulePreview": {}, + "runHistoryLimit": 50 + } + }] +} diff --git a/types/version/message-checks.ts b/types/version/message-checks.ts index 8da4dddf5..5ccf02870 100644 --- a/types/version/message-checks.ts +++ b/types/version/message-checks.ts @@ -76,7 +76,15 @@ type _ExpectedCommands = | 'resolveSessionConfig' | 'sessionConfigCompletions' | 'completions' - | 'invokeChangesetOperation'; + | 'invokeChangesetOperation' + | 'listAutomations' + | 'listAutomationTriggerDefinitions' + | 'createAutomation' + | 'updateAutomation' + | 'disposeAutomation' + | 'runAutomation' + | 'fetchAutomationRuns' + | 'previewAutomationSchedule'; /** All methods annotated `@messageType Notification` (client → server). */ type _ExpectedClientNotifications = @@ -89,6 +97,9 @@ type _ExpectedServerNotifications = | 'root/sessionAdded' | 'root/sessionRemoved' | 'root/sessionSummaryChanged' + | 'root/automationAdded' + | 'root/automationRemoved' + | 'root/automationSummaryChanged' | 'root/progress' | 'auth/required' | 'otlp/exportLogs' diff --git a/types/version/registry.ts b/types/version/registry.ts index 205e00e88..3de5ab43d 100644 --- a/types/version/registry.ts +++ b/types/version/registry.ts @@ -164,6 +164,17 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.TerminalCommandExecuted]: '0.1.0', [ActionType.TerminalCommandFinished]: '0.1.0', [ActionType.ResourceWatchChanged]: '0.2.0', + [ActionType.AutomationDefinitionChanged]: '0.8.0', + [ActionType.AutomationRunSummarySet]: '0.8.0', + [ActionType.AutomationRunSummaryRemoved]: '0.8.0', + [ActionType.AutomationRunsLoaded]: '0.8.0', + [ActionType.AutomationRunLifecycleChanged]: '0.8.0', + [ActionType.AutomationRunSessionSet]: '0.8.0', + [ActionType.AutomationRunSessionRemoved]: '0.8.0', + [ActionType.AutomationRunPrimarySessionChanged]: '0.8.0', + [ActionType.AutomationRunArtifactSet]: '0.8.0', + [ActionType.AutomationRunArtifactRemoved]: '0.8.0', + [ActionType.AutomationRunCancelRequested]: '0.8.0', }; /** @@ -194,6 +205,9 @@ export const NOTIFICATION_INTRODUCED_IN: { readonly [K in ProtocolNotificationMe 'root/sessionAdded': '0.1.0', 'root/sessionRemoved': '0.1.0', 'root/sessionSummaryChanged': '0.1.0', + 'root/automationAdded': '0.8.0', + 'root/automationRemoved': '0.8.0', + 'root/automationSummaryChanged': '0.8.0', 'root/progress': '0.5.0', 'auth/required': '0.1.0', 'otlp/exportLogs': '0.2.0', From 2eed06c75b3743a26ae3cd50059e468cc59555f9 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Wed, 5 Aug 2026 17:40:18 +0200 Subject: [PATCH 2/8] automations: feat: preserve session model and agent selection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 935e37c8-2b68-40b1-9273-f195cb3af9a0 --- clients/go/ahptypes/state.generated.go | 4 +++- .../agenthostprotocol/generated/State.generated.kt | 2 ++ clients/rust/crates/ahp-types/src/state.rs | 4 ++++ .../AgentHostProtocol/Generated/State.generated.swift | 6 ++++++ docs/specification/automation-channel.md | 5 ++++- schema/actions.schema.json | 6 ++++++ schema/commands.schema.json | 6 ++++++ schema/errors.schema.json | 6 ++++++ schema/notifications.schema.json | 6 ++++++ schema/state.schema.json | 6 ++++++ types/channels-automation/state.ts | 4 ++++ 11 files changed, 53 insertions(+), 2 deletions(-) diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 5a97c79ca..639b3b0b0 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -3723,7 +3723,9 @@ type AutomationTriggerDefinition struct { } type AutomationSessionTemplate struct { - Provider *string `json:"provider,omitempty"` + Provider *string `json:"provider,omitempty"` + Model *ModelSelection `json:"model,omitempty"` + Agent *AgentSelection `json:"agent,omitempty"` // Absence means a workspace-less session. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Values resolved through `resolveSessionConfig`. 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 98f4682c3..0609ab946 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 @@ -4886,6 +4886,8 @@ data class AutomationTriggerDefinition( @Serializable data class AutomationSessionTemplate( val provider: String? = null, + val model: ModelSelection? = null, + val agent: AgentSelection? = null, /** * Absence means a workspace-less session. */ diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 10eeea8f3..5f4858591 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -4470,6 +4470,10 @@ pub struct AutomationTriggerDefinition { pub struct AutomationSessionTemplate { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option, /// Absence means a workspace-less session. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index a30964e79..5ad3d9415 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -5484,6 +5484,8 @@ public struct AutomationTriggerDefinition: Codable, Sendable { public struct AutomationSessionTemplate: Codable, Sendable { public var provider: String? + public var model: ModelSelection? + public var agent: AgentSelection? /// Absence means a workspace-less session. public var workingDirectories: [String]? /// Values resolved through `resolveSessionConfig`. @@ -5491,10 +5493,14 @@ public struct AutomationSessionTemplate: Codable, Sendable { public init( provider: String? = nil, + model: ModelSelection? = nil, + agent: AgentSelection? = nil, workingDirectories: [String]? = nil, config: [String: AnyCodable]? = nil ) { self.provider = provider + self.model = model + self.agent = agent self.workingDirectories = workingDirectories self.config = config } diff --git a/docs/specification/automation-channel.md b/docs/specification/automation-channel.md index c64b4b3ed..e005188ac 100644 --- a/docs/specification/automation-channel.md +++ b/docs/specification/automation-channel.md @@ -22,6 +22,10 @@ An empty trigger list means manual-only. Schedule triggers are typed; event triggers use a host-defined type plus schema-defined configuration returned by `listAutomationTriggerDefinitions`. +The session template can select a provider, model, and custom agent, and carries +the same schema-defined configuration values used for ordinary session +creation. Hosts revalidate all selections when a run starts. + ## Catalogue Clients fetch summaries through `listAutomations` on `ahp-root://`. Root @@ -65,4 +69,3 @@ occurrence with at most one run. Definitions contain no credentials or durable permission grants. The host authorizes every operation and revalidates session configuration at execution time. - diff --git a/schema/actions.schema.json b/schema/actions.schema.json index ae49340ef..a93290b73 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -7495,6 +7495,12 @@ "provider": { "type": "string" }, + "model": { + "$ref": "#/$defs/ModelSelection" + }, + "agent": { + "$ref": "#/$defs/AgentSelection" + }, "workingDirectories": { "type": "array", "items": { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 9094eecc2..7bceb23c3 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -6964,6 +6964,12 @@ "provider": { "type": "string" }, + "model": { + "$ref": "#/$defs/ModelSelection" + }, + "agent": { + "$ref": "#/$defs/AgentSelection" + }, "workingDirectories": { "type": "array", "items": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 44b0c54e9..094344576 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -5216,6 +5216,12 @@ "provider": { "type": "string" }, + "model": { + "$ref": "#/$defs/ModelSelection" + }, + "agent": { + "$ref": "#/$defs/AgentSelection" + }, "workingDirectories": { "type": "array", "items": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index b05836290..b573a1741 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -5437,6 +5437,12 @@ "provider": { "type": "string" }, + "model": { + "$ref": "#/$defs/ModelSelection" + }, + "agent": { + "$ref": "#/$defs/AgentSelection" + }, "workingDirectories": { "type": "array", "items": { diff --git a/schema/state.schema.json b/schema/state.schema.json index 0734f52f8..1226c99f0 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -5127,6 +5127,12 @@ "provider": { "type": "string" }, + "model": { + "$ref": "#/$defs/ModelSelection" + }, + "agent": { + "$ref": "#/$defs/AgentSelection" + }, "workingDirectories": { "type": "array", "items": { diff --git a/types/channels-automation/state.ts b/types/channels-automation/state.ts index cbf9a8d4e..64c95d42a 100644 --- a/types/channels-automation/state.ts +++ b/types/channels-automation/state.ts @@ -7,6 +7,8 @@ import type { Message } from '../channels-chat/state.js'; import type { ConfigSchema, URI } from '../common/state.js'; import type { AutomationRunSummary } from '../channels-automation-run/state.js'; +import type { ModelSelection } from '../channels-root/state.js'; +import type { AgentSelection } from '../channels-session/state.js'; /** @category Automation State */ export const enum AutomationOperation { @@ -142,6 +144,8 @@ export interface AutomationTriggerDefinition { /** @category Automation State */ export interface AutomationSessionTemplate { provider?: string; + model?: ModelSelection; + agent?: AgentSelection; /** Absence means a workspace-less session. */ workingDirectories?: URI[]; /** Values resolved through `resolveSessionConfig`. */ From 8ebada14df295bcada2983c57e35f9b921b14c53 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Wed, 5 Aug 2026 18:19:00 +0200 Subject: [PATCH 3/8] automations: fix: remove trailing protocol whitespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 935e37c8-2b68-40b1-9273-f195cb3af9a0 --- types/channels-automation/actions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/channels-automation/actions.ts b/types/channels-automation/actions.ts index 011b502dd..7c4c2df19 100644 --- a/types/channels-automation/actions.ts +++ b/types/channels-automation/actions.ts @@ -36,4 +36,3 @@ export interface AutomationRunsLoadedAction { runs: AutomationRunSummary[]; nextCursor?: string; } - From e69dec71bc3507510f5f82508f42d6b4e33b7092 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Sun, 9 Aug 2026 22:52:35 +0200 Subject: [PATCH 4/8] automations: test: align schema assertions with protocol base Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1131c2f6-bcd0-4012-9265-fdb2685179e7 --- scripts/generate-json-schema.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index dc3332576..d3ee7150f 100644 --- a/scripts/generate-json-schema.test.ts +++ b/scripts/generate-json-schema.test.ts @@ -173,20 +173,6 @@ describe('generated JSON schemas', () => { ); }); - it('inherits request metadata from BaseParams', () => { - if (file !== 'commands.schema.json') { - return; - } - const defs = schema.$defs as Record>; - for (const name of ['BaseParams', 'CreateSessionParams', 'PingParams']) { - const properties = defs[name].properties as Record>; - assert.equal(properties._meta.type, 'object'); - assert.deepEqual(properties._meta.additionalProperties, {}); - } - const baseProperties = defs.BaseParams.properties as Record>; - assert.match(baseProperties._meta.description as string, /Receivers MUST ignore keys/); - }); - it('preserves nested automation capability objects', () => { if (file !== 'commands.schema.json') { return; From 959d5ce9baf923863078cbcff591dce86cc19a9c Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Tue, 11 Aug 2026 14:11:25 +0200 Subject: [PATCH 5/8] automations: fix: regenerate clients after protocol rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1131c2f6-bcd0-4012-9265-fdb2685179e7 --- clients/go/ahptypes/commands.generated.go | 52 ++++++++--- .../generated/Commands.generated.kt | 50 ++++++++++- clients/rust/crates/ahp-types/src/commands.rs | 32 +++++++ .../Generated/Commands.generated.swift | 90 ++++++++++++++++++- schema/commands.schema.json | 40 +++++++++ schema/errors.schema.json | 40 +++++++++ 6 files changed, 288 insertions(+), 16 deletions(-) diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index b3f19c727..3f1fdf4ef 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -1214,6 +1214,9 @@ type ChangesetOperationFollowUp struct { type ListAutomationsParams struct { // Channel URI this command targets. Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Maximum number of entries to return in this page. The server SHOULD respect // this bound but MAY return fewer entries and MAY impose its own upper cap. // Omit to let the server choose the page size. @@ -1236,7 +1239,10 @@ type ListAutomationsResult struct { type ListAutomationTriggerDefinitionsParams struct { // Channel URI this command targets. - Channel URI `json:"channel"` + Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` Provider *string `json:"provider,omitempty"` WorkingDirectories []URI `json:"workingDirectories,omitempty"` SessionConfig map[string]json.RawMessage `json:"sessionConfig,omitempty"` @@ -1248,9 +1254,12 @@ type ListAutomationTriggerDefinitionsResult struct { type CreateAutomationParams struct { // Channel URI this command targets. - Channel URI `json:"channel"` - Definition AutomationDefinition `json:"definition"` - Import *json.RawMessage `json:"import,omitempty"` + Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Definition AutomationDefinition `json:"definition"` + Import *json.RawMessage `json:"import,omitempty"` } type AutomationDefinitionPatch struct { @@ -1264,20 +1273,29 @@ type AutomationDefinitionPatch struct { type UpdateAutomationParams struct { // Channel URI this command targets. - Channel URI `json:"channel"` - ExpectedRevision int64 `json:"expectedRevision"` - Changes AutomationDefinitionPatch `json:"changes"` + Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + ExpectedRevision int64 `json:"expectedRevision"` + Changes AutomationDefinitionPatch `json:"changes"` } type DisposeAutomationParams struct { // Channel URI this command targets. Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } type RunAutomationParams struct { // Channel URI this command targets. - Channel URI `json:"channel"` - RequestId string `json:"requestId"` + Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + RequestId string `json:"requestId"` } type RunAutomationResult struct { @@ -1286,8 +1304,11 @@ type RunAutomationResult struct { type FetchAutomationRunsParams struct { // Channel URI this command targets. - Channel URI `json:"channel"` - Cursor *string `json:"cursor,omitempty"` + Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Cursor *string `json:"cursor,omitempty"` } type FetchAutomationRunsResult struct { @@ -1295,9 +1316,12 @@ type FetchAutomationRunsResult struct { type PreviewAutomationScheduleParams struct { // Channel URI this command targets. - Channel URI `json:"channel"` - Schedule AutomationSchedule `json:"schedule"` - Count *int64 `json:"count,omitempty"` + Channel URI `json:"channel"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Schedule AutomationSchedule `json:"schedule"` + Count *int64 `json:"count,omitempty"` } type PreviewAutomationScheduleResult struct { diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 62fca22d9..e3f63014b 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -1544,6 +1544,12 @@ data class ListAutomationsParams( * Channel URI this command targets. */ val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null, /** * Maximum number of entries to return in this page. The server SHOULD respect * this bound but MAY return fewer entries and MAY impose its own upper cap. @@ -1577,6 +1583,12 @@ data class ListAutomationTriggerDefinitionsParams( * Channel URI this command targets. */ val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null, val provider: String? = null, val workingDirectories: List? = null, val sessionConfig: Map? = null @@ -1593,6 +1605,12 @@ data class CreateAutomationParams( * Channel URI this command targets. */ val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null, val definition: AutomationDefinition, @SerialName("import") val `import`: JsonElement? = null @@ -1615,6 +1633,12 @@ data class UpdateAutomationParams( * Channel URI this command targets. */ val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null, val expectedRevision: Long, val changes: AutomationDefinitionPatch ) @@ -1624,7 +1648,13 @@ data class DisposeAutomationParams( /** * Channel URI this command targets. */ - val channel: String + val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null ) @Serializable @@ -1633,6 +1663,12 @@ data class RunAutomationParams( * Channel URI this command targets. */ val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null, val requestId: String ) @@ -1647,6 +1683,12 @@ data class FetchAutomationRunsParams( * Channel URI this command targets. */ val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null, val cursor: String? = null ) @@ -1659,6 +1701,12 @@ data class PreviewAutomationScheduleParams( * Channel URI this command targets. */ val channel: String, + /** + * Optional JSON-serializable metadata associated with this request. + * Receivers MUST ignore keys they do not understand. + */ + @SerialName("_meta") + val meta: Map? = null, val schedule: AutomationSchedule, val count: Long? = null ) diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index f172886f3..d5d147f31 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -1488,6 +1488,10 @@ pub struct ChangesetOperationFollowUp { pub struct ListAutomationsParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, /// Maximum number of entries to return in this page. The server SHOULD respect /// this bound but MAY return fewer entries and MAY impose its own upper cap. /// Omit to let the server choose the page size. @@ -1519,6 +1523,10 @@ pub struct ListAutomationsResult { pub struct ListAutomationTriggerDefinitionsParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1538,6 +1546,10 @@ pub struct ListAutomationTriggerDefinitionsResult { pub struct CreateAutomationParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, pub definition: AutomationDefinition, #[serde(default, skip_serializing_if = "Option::is_none")] pub import: Option, @@ -1565,6 +1577,10 @@ pub struct AutomationDefinitionPatch { pub struct UpdateAutomationParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, pub expected_revision: i64, pub changes: AutomationDefinitionPatch, } @@ -1574,6 +1590,10 @@ pub struct UpdateAutomationParams { pub struct DisposeAutomationParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -1581,6 +1601,10 @@ pub struct DisposeAutomationParams { pub struct RunAutomationParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, pub request_id: String, } @@ -1595,6 +1619,10 @@ pub struct RunAutomationResult { pub struct FetchAutomationRunsParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } @@ -1608,6 +1636,10 @@ pub struct FetchAutomationRunsResult {} pub struct PreviewAutomationScheduleParams { /// Channel URI this command targets. pub channel: Uri, + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + pub meta: Option, pub schedule: AutomationSchedule, #[serde(default, skip_serializing_if = "Option::is_none")] pub count: Option, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index f670b2eed..b84c8a8e0 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -1912,6 +1912,9 @@ public struct ChangesetOperationFollowUp: Codable, Sendable { public struct ListAutomationsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? /// Maximum number of entries to return in this page. The server SHOULD respect /// this bound but MAY return fewer entries and MAY impose its own upper cap. /// Omit to let the server choose the page size. @@ -1923,13 +1926,23 @@ public struct ListAutomationsParams: Codable, Sendable { public var cursor: String? public var enabled: Bool? + enum CodingKeys: String, CodingKey { + case channel + case meta = "_meta" + case limit + case cursor + case enabled + } + public init( channel: String, + meta: [String: AnyCodable]? = nil, limit: Int? = nil, cursor: String? = nil, enabled: Bool? = nil ) { self.channel = channel + self.meta = meta self.limit = limit self.cursor = cursor self.enabled = enabled @@ -1955,17 +1968,30 @@ public struct ListAutomationsResult: Codable, Sendable { public struct ListAutomationTriggerDefinitionsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? public var provider: String? public var workingDirectories: [String]? public var sessionConfig: [String: AnyCodable]? + enum CodingKeys: String, CodingKey { + case channel + case meta = "_meta" + case provider + case workingDirectories + case sessionConfig + } + public init( channel: String, + meta: [String: AnyCodable]? = nil, provider: String? = nil, workingDirectories: [String]? = nil, sessionConfig: [String: AnyCodable]? = nil ) { self.channel = channel + self.meta = meta self.provider = provider self.workingDirectories = workingDirectories self.sessionConfig = sessionConfig @@ -1985,21 +2011,27 @@ public struct ListAutomationTriggerDefinitionsResult: Codable, Sendable { public struct CreateAutomationParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? public var definition: AutomationDefinition public var `import`: AnyCodable? enum CodingKeys: String, CodingKey { case channel + case meta = "_meta" case definition case `import` = "import" } public init( channel: String, + meta: [String: AnyCodable]? = nil, definition: AutomationDefinition, `import`: AnyCodable? = nil ) { self.channel = channel + self.meta = meta self.definition = definition self.`import` = `import` } @@ -2042,15 +2074,27 @@ public struct AutomationDefinitionPatch: Codable, Sendable { public struct UpdateAutomationParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? public var expectedRevision: Int public var changes: AutomationDefinitionPatch + enum CodingKeys: String, CodingKey { + case channel + case meta = "_meta" + case expectedRevision + case changes + } + public init( channel: String, + meta: [String: AnyCodable]? = nil, expectedRevision: Int, changes: AutomationDefinitionPatch ) { self.channel = channel + self.meta = meta self.expectedRevision = expectedRevision self.changes = changes } @@ -2059,24 +2103,45 @@ public struct UpdateAutomationParams: Codable, Sendable { public struct DisposeAutomationParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case channel + case meta = "_meta" + } public init( - channel: String + channel: String, + meta: [String: AnyCodable]? = nil ) { self.channel = channel + self.meta = meta } } public struct RunAutomationParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? public var requestId: String + enum CodingKeys: String, CodingKey { + case channel + case meta = "_meta" + case requestId + } + public init( channel: String, + meta: [String: AnyCodable]? = nil, requestId: String ) { self.channel = channel + self.meta = meta self.requestId = requestId } } @@ -2094,13 +2159,24 @@ public struct RunAutomationResult: Codable, Sendable { public struct FetchAutomationRunsParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? public var cursor: String? + enum CodingKeys: String, CodingKey { + case channel + case meta = "_meta" + case cursor + } + public init( channel: String, + meta: [String: AnyCodable]? = nil, cursor: String? = nil ) { self.channel = channel + self.meta = meta self.cursor = cursor } } @@ -2116,15 +2192,27 @@ public struct FetchAutomationRunsResult: Codable, Sendable { public struct PreviewAutomationScheduleParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String + /// Optional JSON-serializable metadata associated with this request. + /// Receivers MUST ignore keys they do not understand. + public var meta: [String: AnyCodable]? public var schedule: AutomationSchedule public var count: Int? + enum CodingKeys: String, CodingKey { + case channel + case meta = "_meta" + case schedule + case count + } + public init( channel: String, + meta: [String: AnyCodable]? = nil, schedule: AutomationSchedule, count: Int? = nil ) { self.channel = channel + self.meta = meta self.schedule = schedule self.count = count } diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 7bceb23c3..b65c4987e 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1594,6 +1594,11 @@ "ahp-root://" ] }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "limit": { "type": "number", "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." @@ -1637,6 +1642,11 @@ "ahp-root://" ] }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "provider": { "type": "string" }, @@ -1675,6 +1685,11 @@ "channel": { "$ref": "#/$defs/URI" }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "definition": { "$ref": "#/$defs/AutomationDefinition" }, @@ -1736,6 +1751,11 @@ "channel": { "$ref": "#/$defs/URI" }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "expectedRevision": { "type": "number" }, @@ -1755,6 +1775,11 @@ "channel": { "$ref": "#/$defs/URI", "description": "Channel URI this command targets." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." } }, "required": [ @@ -1768,6 +1793,11 @@ "$ref": "#/$defs/URI", "description": "Channel URI this command targets." }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "requestId": { "type": "string" } @@ -1795,6 +1825,11 @@ "$ref": "#/$defs/URI", "description": "Channel URI this command targets." }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "cursor": { "type": "string" } @@ -1816,6 +1851,11 @@ "ahp-root://" ] }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "schedule": { "$ref": "#/$defs/AutomationSchedule" }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 094344576..db5ac9489 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -7294,6 +7294,11 @@ "ahp-root://" ] }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "limit": { "type": "number", "description": "Maximum number of entries to return in this page. The server SHOULD respect\nthis bound but MAY return fewer entries and MAY impose its own upper cap.\nOmit to let the server choose the page size." @@ -7337,6 +7342,11 @@ "ahp-root://" ] }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "provider": { "type": "string" }, @@ -7375,6 +7385,11 @@ "channel": { "$ref": "#/$defs/URI" }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "definition": { "$ref": "#/$defs/AutomationDefinition" }, @@ -7436,6 +7451,11 @@ "channel": { "$ref": "#/$defs/URI" }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "expectedRevision": { "type": "number" }, @@ -7455,6 +7475,11 @@ "channel": { "$ref": "#/$defs/URI", "description": "Channel URI this command targets." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." } }, "required": [ @@ -7468,6 +7493,11 @@ "$ref": "#/$defs/URI", "description": "Channel URI this command targets." }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "requestId": { "type": "string" } @@ -7495,6 +7525,11 @@ "$ref": "#/$defs/URI", "description": "Channel URI this command targets." }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "cursor": { "type": "string" } @@ -7516,6 +7551,11 @@ "ahp-root://" ] }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, "schedule": { "$ref": "#/$defs/AutomationSchedule" }, From 540a9dc50bf0c56df99083cb710d402f0b125e3e Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Wed, 12 Aug 2026 17:50:49 +0200 Subject: [PATCH 6/8] automations: fix: validate client-dispatchable automation actions isClientDispatchable did not accept automation or automation-run actions, so servers could not use it to reject server-only actions arriving on the new automation channels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1131c2f6-bcd0-4012-9265-fdb2685179e7 --- types/common/reducer-helpers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/types/common/reducer-helpers.ts b/types/common/reducer-helpers.ts index 7742f2af9..02a9d46e2 100644 --- a/types/common/reducer-helpers.ts +++ b/types/common/reducer-helpers.ts @@ -16,6 +16,10 @@ import type { ClientChangesetAction, AnnotationsAction, ClientAnnotationsAction, + AutomationAction, + ClientAutomationAction, + AutomationRunAction, + ClientAutomationRunAction, } from '../action-origin.generated.js'; import { IS_CLIENT_DISPATCHABLE } from '../action-origin.generated.js'; @@ -40,6 +44,6 @@ export function softAssertNever(value: never, log?: (msg: string) => void): void * Servers SHOULD call this to validate incoming `dispatchAction` requests * and reject any action the client is not allowed to originate. */ -export function isClientDispatchable(action: RootAction | SessionAction | TerminalAction | ChangesetAction | AnnotationsAction): action is ClientRootAction | ClientSessionAction | ClientTerminalAction | ClientChangesetAction | ClientAnnotationsAction { +export function isClientDispatchable(action: RootAction | SessionAction | TerminalAction | ChangesetAction | AnnotationsAction | AutomationAction | AutomationRunAction): action is ClientRootAction | ClientSessionAction | ClientTerminalAction | ClientChangesetAction | ClientAnnotationsAction | ClientAutomationAction | ClientAutomationRunAction { return IS_CLIENT_DISPATCHABLE[action.type]; } From a86430694f465cb8dfdd485b55da129303b7d1c3 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Wed, 12 Aug 2026 21:25:51 +0200 Subject: [PATCH 7/8] automations: refactor: document portable cron scheduling Collapse recurring schedules to the specified five-field AHP cron format, document the automation and run protocol surfaces, expand the guide, and regenerate every client and schema. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1131c2f6-bcd0-4012-9265-fdb2685179e7 --- clients/go/ahp/hosts/hosts.go | 11 +- clients/go/ahptypes/actions.generated.go | 102 ++- clients/go/ahptypes/ahptypes_test.go | 21 - clients/go/ahptypes/commands.generated.go | 199 ++++- .../go/ahptypes/notifications.generated.go | 21 +- clients/go/ahptypes/state.generated.go | 613 +++++++++------- .../generated/Actions.generated.kt | 45 ++ .../generated/Commands.generated.kt | 124 +++- .../generated/Notifications.generated.kt | 18 + .../generated/State.generated.kt | 528 ++++++++++---- .../DiscriminatedUnionTest.kt | 19 - clients/rust/crates/ahp-types/src/actions.rs | 60 ++ clients/rust/crates/ahp-types/src/commands.rs | 150 +++- .../crates/ahp-types/src/notifications.rs | 15 + clients/rust/crates/ahp-types/src/state.rs | 351 ++++++--- .../Generated/Actions.generated.swift | 15 + .../Generated/Commands.generated.swift | 78 +- .../Generated/Notifications.generated.swift | 6 + .../Generated/State.generated.swift | 310 ++++---- .../AutomationUnionTests.swift | 16 - docs/guide/automations.md | 493 ++++++++++++- docs/specification/automation-channel.md | 14 +- schema/actions.schema.json | 484 ++++++------ schema/commands.schema.json | 690 ++++++++++-------- schema/errors.schema.json | 690 ++++++++++-------- schema/notifications.schema.json | 449 ++++++------ schema/state.schema.json | 428 +++++------ scripts/generate-go.ts | 29 +- scripts/generate-json-schema.test.ts | 8 +- scripts/generate-kotlin.ts | 26 +- scripts/generate-rust.ts | 30 +- scripts/generate-swift.ts | 26 +- types/channels-automation-run/actions.ts | 72 +- types/channels-automation-run/state.ts | 202 ++++- types/channels-automation/actions.ts | 56 +- types/channels-automation/commands.ts | 158 +++- types/channels-automation/state.ts | 339 ++++++--- types/channels-root/notifications.ts | 21 + types/channels-session/state.ts | 25 +- types/common/commands.ts | 84 ++- .../round-trips/041-automation-snapshot.json | 12 +- .../043-automation-capabilities.json | 6 +- 42 files changed, 4713 insertions(+), 2331 deletions(-) delete mode 100644 clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift diff --git a/clients/go/ahp/hosts/hosts.go b/clients/go/ahp/hosts/hosts.go index 2e50e6f3e..f5febed69 100644 --- a/clients/go/ahp/hosts/hosts.go +++ b/clients/go/ahp/hosts/hosts.go @@ -739,14 +739,9 @@ func cloneAutomationCapabilities(capabilities *ahptypes.AutomationCapabilities) } if capabilities.Schedules != nil { value := *capabilities.Schedules - value.Kinds = append([]ahptypes.AutomationScheduleKind(nil), capabilities.Schedules.Kinds...) - if capabilities.Schedules.Cron != nil { - cron := *capabilities.Schedules.Cron - if capabilities.Schedules.Cron.MinIntervalMinutes != nil { - minIntervalMinutes := *capabilities.Schedules.Cron.MinIntervalMinutes - cron.MinIntervalMinutes = &minIntervalMinutes - } - value.Cron = &cron + if capabilities.Schedules.MinIntervalMinutes != nil { + minIntervalMinutes := *capabilities.Schedules.MinIntervalMinutes + value.MinIntervalMinutes = &minIntervalMinutes } clone.Schedules = &value } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 17f33c315..935b103fa 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -1491,61 +1491,121 @@ type ResourceWatchChangedAction struct { Changes json.RawMessage `json:"changes"` } +// Replace the editable definition after a successful `updateAutomation` or +// another host-authorized definition change. +// +// Full replacement semantics apply to `definition`. The reducer also replaces +// the revision and modification timestamp. Omitting `nextRunAt` clears the +// previously projected next occurrence. type AutomationDefinitionChangedAction struct { - Type ActionType `json:"type"` + Type ActionType `json:"type"` + // Complete replacement definition. Definition AutomationDefinition `json:"definition"` - Revision int64 `json:"revision"` - ModifiedAt string `json:"modifiedAt"` - NextRunAt *string `json:"nextRunAt,omitempty"` + // New monotonic revision. + Revision int64 `json:"revision"` + // Definition modification timestamp in ISO 8601 format. + ModifiedAt string `json:"modifiedAt"` + // Earliest known future scheduled occurrence, or omitted to clear it. + NextRunAt *string `json:"nextRunAt,omitempty"` } +// Upsert one run summary in the retained history. +// +// Existing entries are replaced by {@link AutomationRunSummary.resource}. A +// previously unseen run is inserted at the front because history is +// newest-first. type AutomationRunSummarySetAction struct { - Type ActionType `json:"type"` - Run AutomationRunSummary `json:"run"` + Type ActionType `json:"type"` + // New or replacement run summary. + Run AutomationRunSummary `json:"run"` } +// Remove one retained run summary by its automation-run URI. +// +// The action is a no-op when the URI is not present in the current history +// window. type AutomationRunSummaryRemovedAction struct { Type ActionType `json:"type"` - Run URI `json:"run"` + // {@link AutomationRunSummary.resource} to remove. + Run URI `json:"run"` } +// Append an older page of run summaries returned by +// `fetchAutomationRuns`. +// +// Entries already present by resource URI are ignored, preserving the +// newest-first ordering of the existing history followed by the fetched page. +// Omitting `nextCursor` marks the end of retained history. type AutomationRunsLoadedAction struct { - Type ActionType `json:"type"` - Runs []AutomationRunSummary `json:"runs"` - NextCursor *string `json:"nextCursor,omitempty"` + Type ActionType `json:"type"` + // Older run summaries in newest-first order within this page. + Runs []AutomationRunSummary `json:"runs"` + // Opaque cursor for the next older page, or omitted at the end. + NextCursor *string `json:"nextCursor,omitempty"` } +// Replace the run lifecycle and currently allowed operations atomically. +// +// The host dispatches this action for every lifecycle transition. Terminal +// lifecycles normally carry an empty operations list. type AutomationRunLifecycleChangedAction struct { - Type ActionType `json:"type"` - Lifecycle AutomationRunLifecycle `json:"lifecycle"` + Type ActionType `json:"type"` + // Complete replacement lifecycle. + Lifecycle AutomationRunLifecycle `json:"lifecycle"` + // Complete replacement operation list. Operations []AutomationRunOperation `json:"operations"` } +// Add a session to the run's ordered session catalogue. +// +// Session URIs are unique. Setting an existing URI is a no-op. type AutomationRunSessionSetAction struct { - Type ActionType `json:"type"` - Session URI `json:"session"` + Type ActionType `json:"type"` + // Session URI to append when it is not already linked. + Session URI `json:"session"` } +// Remove a linked session from the run. +// +// Removing the current primary session also clears +// {@link AutomationRunState.primarySession}. An unknown URI is a no-op. type AutomationRunSessionRemovedAction struct { - Type ActionType `json:"type"` - Session URI `json:"session"` + Type ActionType `json:"type"` + // Linked session URI to remove. + Session URI `json:"session"` } +// Select or clear the session clients should open first for this run. type AutomationRunPrimarySessionChangedAction struct { - Type ActionType `json:"type"` - PrimarySession *URI `json:"primarySession,omitempty"` + Type ActionType `json:"type"` + // New primary linked session, or omitted to clear the selection. + PrimarySession *URI `json:"primarySession,omitempty"` } +// Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}. type AutomationRunArtifactSetAction struct { - Type ActionType `json:"type"` + Type ActionType `json:"type"` + // New or replacement artifact. Artifact AutomationRunArtifact `json:"artifact"` } +// Remove a run-scoped artifact by id. +// +// The action is a no-op when the id is not present. type AutomationRunArtifactRemovedAction struct { - Type ActionType `json:"type"` - ArtifactId string `json:"artifactId"` + Type ActionType `json:"type"` + // {@link AutomationRunArtifact.id} to remove. + ArtifactId string `json:"artifactId"` } +// Ask the host to cancel this run. +// +// This is the only client-dispatchable automation-run action. It is a +// side-effect request and deliberately leaves optimistic state unchanged. The +// authoritative outcome arrives later through +// {@link AutomationRunLifecycleChangedAction}: cancellation may transition to +// `cancelled`, or the run may complete or fail before cancellation takes +// effect. type AutomationRunCancelRequestedAction struct { Type ActionType `json:"type"` } diff --git a/clients/go/ahptypes/ahptypes_test.go b/clients/go/ahptypes/ahptypes_test.go index 2422c27e4..8970c3e36 100644 --- a/clients/go/ahptypes/ahptypes_test.go +++ b/clients/go/ahptypes/ahptypes_test.go @@ -143,27 +143,6 @@ func TestSnapshotStateVariants(t *testing.T) { } } -func TestAutomationScheduleUsesUnionDiscriminator(t *testing.T) { - schedule := AutomationSchedule{ - Value: &AutomationDailySchedule{ - Kind: AutomationScheduleKindHourly, - Time: AutomationLocalTime{Hour: 9, Minute: 0}, - TimeZone: "UTC", - }, - } - data, err := json.Marshal(schedule) - if err != nil { - t.Fatal(err) - } - var wire map[string]any - if err := json.Unmarshal(data, &wire); err != nil { - t.Fatal(err) - } - if wire["kind"] != "daily" { - t.Fatalf("expected daily union discriminator, got %v", wire["kind"]) - } -} - // TestSessionStatusBitset confirms the typed-uint32 Has/Or helpers // match the canonical bitset semantics. func TestSessionStatusBitset(t *testing.T) { diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 3f1fdf4ef..d57673eca 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -167,7 +167,8 @@ type InitializeResult struct { // defines a template variable, `{level}`, for subscriber-side severity // filtering). Clients MAY ignore signals they cannot process. Telemetry *TelemetryCapabilities `json:"telemetry,omitempty"` - // Host automation support. Absence means unsupported. + // Host-owned automation support. Absence means the host does not expose an + // automation catalogue or automation commands. Automations *AutomationCapabilities `json:"automations,omitempty"` } @@ -191,35 +192,63 @@ type ClientCapabilities struct { McpApps map[string]json.RawMessage `json:"mcpApps,omitempty"` } +// Automation features supported by this host authority. +// +// Capabilities describe implementation support. Per-resource +// {@link AutomationState.operations} and +// {@link AutomationRunState.operations} remain authoritative for whether a +// particular operation is currently allowed. type AutomationCapabilities struct { - Execution AutomationExecutionCapabilities `json:"execution"` - Create *AutomationCreateCapability `json:"create,omitempty"` - Schedules *AutomationScheduleCapabilities `json:"schedules,omitempty"` + // Availability guarantee for automatic trigger execution. + Execution AutomationExecutionCapabilities `json:"execution"` + // Present when clients may call `createAutomation`. + Create *AutomationCreateCapability `json:"create,omitempty"` + // Present when definitions may contain schedule triggers. + Schedules *AutomationScheduleCapabilities `json:"schedules,omitempty"` + // Present when clients may request cancellation on eligible runs. RunCancellation *AutomationRunCancellationCapability `json:"runCancellation,omitempty"` + // Present when clients may call `previewAutomationSchedule`. SchedulePreview *AutomationSchedulePreviewCapability `json:"schedulePreview,omitempty"` - RunHistoryLimit *int64 `json:"runHistoryLimit,omitempty"` + // Maximum terminal run summaries retained per automation. Active runs are not + // counted toward the limit. Absence means the retention limit is + // implementation-defined. + RunHistoryLimit *int64 `json:"runHistoryLimit,omitempty"` } +// Automatic trigger execution availability. type AutomationExecutionCapabilities struct { + // How long automatic trigger evaluation remains available. Lifetime AutomationExecutionLifetime `json:"lifetime"` } +// Presence capability for `createAutomation`. +// +// The empty object means "supported"; fields are reserved for future +// create-specific options. type AutomationCreateCapability struct { } +// Host restrictions on portable {@link AutomationSchedule} triggers. +// +// The cron grammar itself is fixed by AHP. Hosts MUST accept every expression +// in that grammar unless it violates an advertised interval restriction. type AutomationScheduleCapabilities struct { - Kinds []AutomationScheduleKind `json:"kinds"` - Cron *AutomationCronScheduleCapability `json:"cron,omitempty"` -} - -type AutomationCronScheduleCapability struct { - Dialect string `json:"dialect"` + // Smallest permitted interval between consecutive occurrences. Omission + // means no restriction beyond the cron format's one-minute resolution. MinIntervalMinutes *int64 `json:"minIntervalMinutes,omitempty"` } +// Presence capability for `automationRun/cancelRequested`. +// +// The empty object means "supported"; clients must additionally check for +// {@link AutomationRunOperation.Cancel} on each run. type AutomationRunCancellationCapability struct { } +// Presence capability for `previewAutomationSchedule`. +// +// The empty object means "supported"; fields are reserved for future preview +// limits or options. type AutomationSchedulePreviewCapability struct { } @@ -1211,6 +1240,12 @@ type ChangesetOperationFollowUp struct { External *bool `json:"external,omitempty"` } +// List the host's automation catalogue without subscribing to every +// automation channel. +// +// Results are lightweight {@link AutomationSummary} entries. Clients SHOULD +// re-run this command after reconnect because root catalogue notifications are +// not replayed. type ListAutomationsParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1225,62 +1260,120 @@ type ListAutomationsParams struct { // Omit to fetch the first page. Cursors are server-defined and MUST be treated // as opaque — do not parse, modify, or persist them across connections. An // unrecognised cursor SHOULD be rejected with an `InvalidParams` error. - Cursor *string `json:"cursor,omitempty"` - Enabled *bool `json:"enabled,omitempty"` + Cursor *string `json:"cursor,omitempty"` + // Optional exact filter on {@link AutomationDefinition.enabled}. + Enabled *bool `json:"enabled,omitempty"` } +// One page of the automation catalogue. type ListAutomationsResult struct { // Opaque cursor for the next page. Present when more entries exist beyond the // returned page; absent signals the end of the collection. Pass it back as // {@link PaginatedParams.cursor} to fetch the following page. - NextCursor *string `json:"nextCursor,omitempty"` - Items []AutomationSummary `json:"items"` + NextCursor *string `json:"nextCursor,omitempty"` + // Automation summaries in host-defined catalogue order. + Items []AutomationSummary `json:"items"` } +// Discover event-trigger types available for a prospective session template. +// +// Hosts may vary definitions by provider, workspace, and session +// configuration. Schedule triggers are protocol-defined and therefore do not +// appear in this result. type ListAutomationTriggerDefinitionsParams struct { // Channel URI this command targets. Channel URI `json:"channel"` // Optional JSON-serializable metadata associated with this request. // Receivers MUST ignore keys they do not understand. - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - Provider *string `json:"provider,omitempty"` - WorkingDirectories []URI `json:"workingDirectories,omitempty"` - SessionConfig map[string]json.RawMessage `json:"sessionConfig,omitempty"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Prospective provider id, or omitted for the host default. + Provider *string `json:"provider,omitempty"` + // Prospective ordered working-directory list. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Prospective resolved session configuration values. + SessionConfig map[string]json.RawMessage `json:"sessionConfig,omitempty"` } +// Host-defined event trigger types available for the supplied context. type ListAutomationTriggerDefinitionsResult struct { + // Available event trigger definitions. Items []AutomationTriggerDefinition `json:"items"` } +// Create a durable automation at a client-chosen URI. +// +// `channel` MUST use the `ahp-automation:` scheme and MUST NOT already identify +// an unrelated automation. The host validates the complete definition, +// persists it, and makes it visible through the root catalogue before +// returning success. type CreateAutomationParams struct { // Channel URI this command targets. Channel URI `json:"channel"` // Optional JSON-serializable metadata associated with this request. // Receivers MUST ignore keys they do not understand. - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - Definition AutomationDefinition `json:"definition"` - Import *json.RawMessage `json:"import,omitempty"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Complete initial definition. + Definition AutomationDefinition `json:"definition"` + // Optional idempotency identity when importing a legacy definition. + Import *AutomationImportIdentity `json:"import,omitempty"` } +// Stable source identity used to make legacy automation import idempotent. +// +// The host remembers this identity independently of the client-chosen +// automation URI. Retrying an interrupted migration with the same values MUST +// resolve to the previously imported item rather than creating a duplicate. +type AutomationImportIdentity struct { + // Stable namespace identifying the source implementation or store. + Source string `json:"source"` + // Identifier shared by every item in one import attempt. + BatchId string `json:"batchId"` + // Stable source-side identifier for this definition within the batch. + ItemId string `json:"itemId"` +} + +// Partial replacement of editable {@link AutomationDefinition} fields. +// +// Omitted fields are unchanged. Supplied arrays and objects replace their +// corresponding values in full; they are not merged recursively. type AutomationDefinitionPatch struct { - Title *string `json:"title,omitempty"` - Message *Message `json:"message,omitempty"` - Session *AutomationSessionTemplate `json:"session,omitempty"` - Enabled *bool `json:"enabled,omitempty"` - Triggers *[]AutomationTrigger `json:"triggers,omitempty"` - Meta *map[string]json.RawMessage `json:"_meta,omitempty"` -} - + // Replacement human-readable title. + Title *string `json:"title,omitempty"` + // Replacement initial user message. + Message *Message `json:"message,omitempty"` + // Replacement session template. + Session *AutomationSessionTemplate `json:"session,omitempty"` + // Replacement automatic-trigger enabled state. + Enabled *bool `json:"enabled,omitempty"` + // Complete replacement trigger list. + Triggers *[]AutomationTrigger `json:"triggers,omitempty"` + // Complete replacement implementation-defined metadata. + Meta *map[string]json.RawMessage `json:"_meta,omitempty"` +} + +// Update editable fields of an existing automation using optimistic +// concurrency. +// +// The host accepts the patch only when `expectedRevision` equals the current +// {@link AutomationState.revision}. A stale revision is rejected; clients +// SHOULD reconcile the latest state before retrying. type UpdateAutomationParams struct { // Channel URI this command targets. Channel URI `json:"channel"` // Optional JSON-serializable metadata associated with this request. // Receivers MUST ignore keys they do not understand. - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - ExpectedRevision int64 `json:"expectedRevision"` - Changes AutomationDefinitionPatch `json:"changes"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Revision on which the client based {@link changes}. + ExpectedRevision int64 `json:"expectedRevision"` + // Editable fields to replace. + Changes AutomationDefinitionPatch `json:"changes"` } +// Permanently remove an automation. +// +// The target is supplied by {@link BaseParams.channel}. The host rejects the +// command when {@link AutomationOperation.Dispose} is not currently +// advertised, for example while a non-terminal run prevents disposal. type DisposeAutomationParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1289,42 +1382,68 @@ type DisposeAutomationParams struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` } +// Start a manual run of an automation. +// +// Manual execution is independent of {@link AutomationDefinition.enabled}. +// The host persists the run before beginning session side effects. type RunAutomationParams struct { // Channel URI this command targets. Channel URI `json:"channel"` // Optional JSON-serializable metadata associated with this request. // Receivers MUST ignore keys they do not understand. - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - RequestId string `json:"requestId"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Durable client-generated idempotency key. Retrying with the same key and + // automation MUST return the original run URI rather than create another + // run. + RequestId string `json:"requestId"` } +// Result identifying the existing or newly created run. type RunAutomationResult struct { + // Subscribable `ahp-automation-run:` URI. Run URI `json:"run"` } +// Load one older page into the subscribed automation's run-history state. +// +// The response only acknowledges the request. Loaded entries arrive through +// `automation/runsLoaded`, keeping all subscribers synchronized through the +// normal action stream. type FetchAutomationRunsParams struct { // Channel URI this command targets. Channel URI `json:"channel"` // Optional JSON-serializable metadata associated with this request. // Receivers MUST ignore keys they do not understand. - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - Cursor *string `json:"cursor,omitempty"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Cursor previously received as {@link AutomationState.runsNextCursor}. + // Omit to request the first page not already included by the snapshot. + Cursor *string `json:"cursor,omitempty"` } +// Empty acknowledgement; run summaries are delivered by action. type FetchAutomationRunsResult struct { } +// Ask the host to evaluate a schedule without creating an automation. +// +// Clients SHOULD use this command for validation and preview instead of +// implementing their own cron evaluator, especially around time-zone +// transitions. type PreviewAutomationScheduleParams struct { // Channel URI this command targets. Channel URI `json:"channel"` // Optional JSON-serializable metadata associated with this request. // Receivers MUST ignore keys they do not understand. - Meta map[string]json.RawMessage `json:"_meta,omitempty"` - Schedule AutomationSchedule `json:"schedule"` - Count *int64 `json:"count,omitempty"` + Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Portable AHP cron schedule to evaluate. + Schedule AutomationSchedule `json:"schedule"` + // Requested maximum number of future occurrences; the host MAY cap it. + Count *int64 `json:"count,omitempty"` } +// Host-canonical future schedule occurrences. type PreviewAutomationScheduleResult struct { + // Ascending ISO 8601 timestamps. Items []string `json:"items"` } diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index b01c55bca..dcdb5c0d2 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -85,18 +85,33 @@ type SessionSummaryChangedParams struct { Changes PartialSessionSummary `json:"changes"` } +// Announces a newly visible automation catalogue entry. +// +// Root notifications are live signals and are not replayed after reconnect. +// Clients that reconnect MUST refresh the catalogue with `listAutomations`. type AutomationAddedParams struct { - Channel URI `json:"channel"` + // Root channel URI. + Channel URI `json:"channel"` + // Complete summary for the newly visible automation. Summary AutomationSummary `json:"summary"` } +// Announces that an automation is no longer present in the root catalogue. type AutomationRemovedParams struct { - Channel URI `json:"channel"` + // Root channel URI. + Channel URI `json:"channel"` + // Removed `ahp-automation:` URI. Automation URI `json:"automation"` } +// Replaces the root-catalogue summary for an existing automation. +// +// Full replacement semantics apply to `summary`; this is not a patch. The +// corresponding subscribed automation channel remains authoritative. type AutomationSummaryChangedParams struct { - Channel URI `json:"channel"` + // Root channel URI. + Channel URI `json:"channel"` + // Complete replacement catalogue summary. Summary AutomationSummary `json:"summary"` } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 639b3b0b0..f39978e31 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -442,92 +442,125 @@ const ( ResourceChangeTypeDeleted ResourceChangeType = "deleted" ) +// Discriminant describing the durable provenance of a session. type SessionOriginKind string const ( + // The session was created as part of an automation run. SessionOriginKindAutomation SessionOriginKind = "automation" ) +// Operations the host currently permits for an automation. +// +// The list on {@link AutomationState.operations} is authoritative and may +// change over time. Clients MUST NOT infer permission from capabilities alone: +// capabilities describe what the host implementation can support, while +// operations describe what is allowed for this particular automation now. type AutomationOperation string const ( - AutomationOperationUpdate AutomationOperation = "update" + // Replace editable fields using `updateAutomation`. + AutomationOperationUpdate AutomationOperation = "update" + // Permanently remove the automation using `disposeAutomation`. AutomationOperationDispose AutomationOperation = "dispose" - AutomationOperationRun AutomationOperation = "run" + // Start a manual run using `runAutomation`. + AutomationOperationRun AutomationOperation = "run" ) +// Availability guarantee for host-owned automatic trigger evaluation. +// +// This describes the authority that owns one automation catalogue. It does not +// prevent a client from connecting to several authorities with different +// lifetimes (for example, one local host and one managed service). type AutomationExecutionLifetime string const ( + // Automatic triggers are evaluated only while this host process is running. + // Definitions may remain durable across restarts, but occurrences while the + // process is unavailable are handled according to the trigger's + // {@link AutomationMisfirePolicy}. AutomationExecutionLifetimeHostLifetime AutomationExecutionLifetime = "hostLifetime" - AutomationExecutionLifetimeManaged AutomationExecutionLifetime = "managed" -) - -type AutomationScheduleKind string - -const ( - AutomationScheduleKindHourly AutomationScheduleKind = "hourly" - AutomationScheduleKindDaily AutomationScheduleKind = "daily" - AutomationScheduleKindWeekly AutomationScheduleKind = "weekly" - AutomationScheduleKindCron AutomationScheduleKind = "cron" -) - -type AutomationWeekday string - -const ( - AutomationWeekdayMonday AutomationWeekday = "monday" - AutomationWeekdayTuesday AutomationWeekday = "tuesday" - AutomationWeekdayWednesday AutomationWeekday = "wednesday" - AutomationWeekdayThursday AutomationWeekday = "thursday" - AutomationWeekdayFriday AutomationWeekday = "friday" - AutomationWeekdaySaturday AutomationWeekday = "saturday" - AutomationWeekdaySunday AutomationWeekday = "sunday" + // Automatic triggers continue to be evaluated independently of connected + // clients and any particular interactive host process. + AutomationExecutionLifetimeManaged AutomationExecutionLifetime = "managed" ) +// How a host handles schedule occurrences missed while automatic execution was +// unavailable. type AutomationMisfirePolicy string const ( - AutomationMisfirePolicySkip AutomationMisfirePolicy = "skip" + // Discard missed occurrences and wait for the next future occurrence. + AutomationMisfirePolicySkip AutomationMisfirePolicy = "skip" + // Start at most one catch-up run when execution becomes available, regardless + // of how many occurrences were missed. AutomationMisfirePolicyRunOnce AutomationMisfirePolicy = "runOnce" ) +// Discriminant for automatic trigger definitions. type AutomationTriggerKind string const ( + // A portable recurring {@link AutomationSchedule}. AutomationTriggerKindSchedule AutomationTriggerKind = "schedule" - AutomationTriggerKindEvent AutomationTriggerKind = "event" + // A host-defined external event discovered from trigger definitions. + AutomationTriggerKindEvent AutomationTriggerKind = "event" ) +// Lifecycle status of one automation run. +// +// `completed`, `failed`, and `cancelled` are terminal. `blocked` is +// non-terminal: the host may return the run to `running` after the linked +// session resolves the blocker. type AutomationRunStatus string const ( - AutomationRunStatusPending AutomationRunStatus = "pending" - AutomationRunStatusRunning AutomationRunStatus = "running" - AutomationRunStatusBlocked AutomationRunStatus = "blocked" + // The durable run record exists but execution has not started. + AutomationRunStatusPending AutomationRunStatus = "pending" + // One or more linked sessions are actively executing. + AutomationRunStatusRunning AutomationRunStatus = "running" + // Execution is paused on an interaction or client-side dependency. + AutomationRunStatusBlocked AutomationRunStatus = "blocked" + // Execution finished successfully. AutomationRunStatusCompleted AutomationRunStatus = "completed" - AutomationRunStatusFailed AutomationRunStatus = "failed" + // Execution ended with an error. + AutomationRunStatusFailed AutomationRunStatus = "failed" + // Execution ended because cancellation was accepted. AutomationRunStatusCancelled AutomationRunStatus = "cancelled" ) +// Coarse reason a run is blocked. +// +// Detailed prompts, confirmations, authentication requests, and tool state +// remain authoritative on linked session and chat channels. type AutomationRunBlockerKind string const ( - AutomationRunBlockerKindUserInput AutomationRunBlockerKind = "userInput" + // A linked session is waiting for an answer to a user-input request. + AutomationRunBlockerKindUserInput AutomationRunBlockerKind = "userInput" + // A linked session is waiting for tool confirmation. AutomationRunBlockerKindToolConfirmation AutomationRunBlockerKind = "toolConfirmation" - AutomationRunBlockerKindAuthentication AutomationRunBlockerKind = "authentication" - AutomationRunBlockerKindClientExecution AutomationRunBlockerKind = "clientExecution" + // Execution requires authentication or renewed credentials. + AutomationRunBlockerKindAuthentication AutomationRunBlockerKind = "authentication" + // Work must be performed by or delegated to a connected client. + AutomationRunBlockerKindClientExecution AutomationRunBlockerKind = "clientExecution" ) +// Discriminant describing what created an automation run. type AutomationRunCauseKind string const ( - AutomationRunCauseKindManual AutomationRunCauseKind = "manual" + // A client explicitly invoked `runAutomation`. + AutomationRunCauseKindManual AutomationRunCauseKind = "manual" + // An automatic schedule or event trigger fired. AutomationRunCauseKindTrigger AutomationRunCauseKind = "trigger" ) +// Operations the host currently permits for a run. type AutomationRunOperation string const ( + // Request cancellation with `automationRun/cancelRequested`. AutomationRunOperationCancel AutomationRunOperation = "cancel" ) @@ -3650,192 +3683,330 @@ type ResourceChange struct { Type ResourceChangeType `json:"type"` } +// Provenance recorded on a session created for an automation run. +// +// The links let clients navigate from an ordinary session to the task-level +// run and its durable definition. The session channel remains authoritative +// for this session's transcript, tools, confirmations, and changes. type AutomationSessionOrigin struct { - Kind SessionOriginKind `json:"kind"` - Automation URI `json:"automation"` - Run URI `json:"run"` -} - -type AutomationLocalTime struct { - Hour int64 `json:"hour"` - Minute int64 `json:"minute"` -} - -type AutomationHourlySchedule struct { - Kind AutomationScheduleKind `json:"kind"` + Kind SessionOriginKind `json:"kind"` + // Owning `ahp-automation:` URI. + Automation URI `json:"automation"` + // Owning `ahp-automation-run:` URI. + Run URI `json:"run"` } -type AutomationDailySchedule struct { - Kind AutomationScheduleKind `json:"kind"` - Time AutomationLocalTime `json:"time"` - // IANA time-zone identifier. - TimeZone string `json:"timeZone"` -} - -type AutomationWeeklySchedule struct { - Kind AutomationScheduleKind `json:"kind"` - Weekday AutomationWeekday `json:"weekday"` - Time AutomationLocalTime `json:"time"` - // IANA time-zone identifier. - TimeZone string `json:"timeZone"` -} - -type AutomationCronSchedule struct { - Kind AutomationScheduleKind `json:"kind"` - // Standard five-field Unix cron expression. +// A portable recurring schedule evaluated in a named time zone. +// +// The expression uses exactly five whitespace-separated fields, in this +// order: +// +// | Field | Values | +// | --- | --- | +// | minute | `0`–`59` | +// | hour | `0`–`23` | +// | day of month | `1`–`31` | +// | month | `1`–`12` or `JAN`–`DEC` | +// | day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday | +// +// Month and weekday names are ASCII and case-insensitive. Each field accepts +// `*`, a single value, an inclusive range (`1-5`), a comma-separated list of +// values or ranges (`1,3,8-10`), or a step applied to `*` or a range (for +// example, */15 or `1-30/2`). A step MUST be a positive integer. AHP does +// not support seconds, years, macros such as `@daily`, or Quartz extensions +// such as `?`, `L`, `W`, and `#`. +// +// Minute, hour, and month must all match. When both day-of-month and +// day-of-week are restricted (not `*`), an occurrence matches when either day +// field matches, following Unix cron semantics. +type AutomationSchedule struct { + // Five-field AHP cron expression described by {@link AutomationSchedule}. Expression string `json:"expression"` - // IANA time-zone identifier. + // IANA Time Zone Database identifier used to interpret the expression, for + // example `"UTC"` or `"Europe/Berlin"`. TimeZone string `json:"timeZone"` } +// Starts runs from a recurring cron schedule evaluated by the host. type AutomationScheduleTrigger struct { - // Stable within the automation definition. - Id string `json:"id"` - Kind AutomationTriggerKind `json:"kind"` - Schedule AutomationSchedule `json:"schedule"` + // Identifier unique and stable within this automation definition. Run causes + // refer back to this value. + Id string `json:"id"` + Kind AutomationTriggerKind `json:"kind"` + // Recurrence and time zone evaluated by the host. + Schedule AutomationSchedule `json:"schedule"` + // Policy for missed occurrences. Omission is equivalent to + // {@link AutomationMisfirePolicy.RunOnce}. MisfirePolicy *AutomationMisfirePolicy `json:"misfirePolicy,omitempty"` } +// Starts runs from events understood by the owning host. +// +// Event trigger types, event ids, and configuration are discovered through +// `listAutomationTriggerDefinitions`. A client that does not understand a +// host-defined trigger can still preserve and display it without interpreting +// its configuration. type AutomationEventTrigger struct { - // Stable within the automation definition. + // Identifier unique and stable within this automation definition. Run causes + // refer back to this value. Id string `json:"id"` Kind AutomationTriggerKind `json:"kind"` - // Stable host-defined trigger type. + // Matches {@link AutomationTriggerDefinition.type}. Type string `json:"type"` - // Selected event actions. + // Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + // trigger type. Events []string `json:"events"` - // Schema-defined values. Unknown entries must survive round-trips. + // Values described by {@link AutomationTriggerDefinition.configSchema}. + // Clients MUST preserve unknown entries when editing other fields. Config map[string]json.RawMessage `json:"config,omitempty"` } +// One selectable event exposed by a host-defined trigger type. type AutomationTriggerEventDefinition struct { - Id string `json:"id"` - Title string `json:"title"` + // Stable event id stored in {@link AutomationEventTrigger.events}. + Id string `json:"id"` + // Human-readable label suitable for selection UI. + Title string `json:"title"` + // Optional longer explanation of when this event fires. Description *string `json:"description,omitempty"` } +// Describes one host-defined event trigger type available for a prospective +// automation session template. +// +// Trigger definitions are discovery metadata, not durable automation state. +// Hosts may return different definitions for different providers, working +// directories, or session configuration. type AutomationTriggerDefinition struct { - Type string `json:"type"` - Title string `json:"title"` - Description *string `json:"description,omitempty"` - Events []AutomationTriggerEventDefinition `json:"events"` - ConfigSchema *ConfigSchema `json:"configSchema,omitempty"` + // Stable type id stored in {@link AutomationEventTrigger.type}. + Type string `json:"type"` + // Human-readable trigger type name. + Title string `json:"title"` + // Optional longer explanation of the trigger source. + Description *string `json:"description,omitempty"` + // Events clients may select for this trigger type. + Events []AutomationTriggerEventDefinition `json:"events"` + // Optional schema for {@link AutomationEventTrigger.config}. + ConfigSchema *ConfigSchema `json:"configSchema,omitempty"` } +// Template from which the host creates a fresh session for each automation run. +// +// The host revalidates every selection when the run starts. Definitions never +// carry credentials, confirmation decisions, or durable permission grants. type AutomationSessionTemplate struct { - Provider *string `json:"provider,omitempty"` - Model *ModelSelection `json:"model,omitempty"` - Agent *AgentSelection `json:"agent,omitempty"` - // Absence means a workspace-less session. + // Provider id. Omit to use the host's default provider. + Provider *string `json:"provider,omitempty"` + // Optional model selection resolved when a run starts. + Model *ModelSelection `json:"model,omitempty"` + // Optional custom agent selection resolved when a run starts. + Agent *AgentSelection `json:"agent,omitempty"` + // Ordered working-directory URIs for each created session. Absence means a + // workspace-less session. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // Values resolved through `resolveSessionConfig`. + // Session configuration values accepted by `createSession`, normally + // obtained from `resolveSessionConfig`. Config map[string]json.RawMessage `json:"config,omitempty"` } +// Durable, client-editable definition of an automation. +// +// A definition combines the initial user message, the session template used +// for each run, and zero or more automatic triggers. Runtime state, run +// history, revisions, timestamps, and currently allowed operations live on +// {@link AutomationState} rather than in the definition. type AutomationDefinition struct { + // Human-readable automation name. Title string `json:"title"` - // Initial user message sent to each new session. - Message Message `json:"message"` + // Initial message sent to every newly created run session. Its origin MUST be + // `user`. + Message Message `json:"message"` + // Template used to create fresh sessions for each run. Session AutomationSessionTemplate `json:"session"` - // Controls automatic triggers; manual runs remain permitted. + // Whether automatic triggers may create runs. Manual runs remain available + // whenever {@link AutomationOperation.Run} is advertised. Enabled bool `json:"enabled"` - // Empty means manual-only. - Triggers []AutomationTrigger `json:"triggers"` - Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Automatic triggers. An empty list means manual-only. + Triggers []AutomationTrigger `json:"triggers"` + // Opaque implementation-defined metadata. Clients MUST preserve unknown + // entries when updating the definition. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } +// Host-resolved execution context that is useful to clients but is not part of +// the editable definition. type AutomationRuntimeState struct { - WorkingDirectories []URI `json:"workingDirectories,omitempty"` - Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Effective working directories after host-side preparation, such as + // materializing a managed workspace. + WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Opaque host-defined runtime metadata. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } +// Lightweight root-catalogue projection of an automation. +// +// Returned by `listAutomations` and carried by root automation notifications, +// this contains enough information to render a list without subscribing to +// every `ahp-automation:` resource. type AutomationSummary struct { - Resource URI `json:"resource"` - Title string `json:"title"` - Enabled bool `json:"enabled"` - TriggerCount int64 `json:"triggerCount"` - NextRunAt *string `json:"nextRunAt,omitempty"` - LastRun *AutomationRunSummary `json:"lastRun,omitempty"` - Revision int64 `json:"revision"` - Operations []AutomationOperation `json:"operations"` - CreatedAt string `json:"createdAt"` - ModifiedAt string `json:"modifiedAt"` - Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Subscribable `ahp-automation:` URI. + Resource URI `json:"resource"` + // Current {@link AutomationDefinition.title}. + Title string `json:"title"` + // Current {@link AutomationDefinition.enabled} value. + Enabled bool `json:"enabled"` + // Number of automatic triggers in the current definition. + TriggerCount int64 `json:"triggerCount"` + // Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + NextRunAt *string `json:"nextRunAt,omitempty"` + // Most recent retained run, when any run exists. + LastRun *AutomationRunSummary `json:"lastRun,omitempty"` + // Monotonic definition revision used for optimistic concurrency. + Revision int64 `json:"revision"` + // Operations currently permitted for this automation. + Operations []AutomationOperation `json:"operations"` + // Creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` + // Last definition modification timestamp in ISO 8601 format. + ModifiedAt string `json:"modifiedAt"` + // Opaque host-defined catalogue metadata. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } +// Authoritative state of one subscribed `ahp-automation:` resource. +// +// The host owns definition revisions, trigger evaluation, run claims, run +// retention, and operation availability. Clients render this state and submit +// commands; they never run a fallback scheduler for a host-owned definition. type AutomationState struct { - Resource URI `json:"resource"` + // URI of this automation channel. + Resource URI `json:"resource"` + // Current durable definition. Definition AutomationDefinition `json:"definition"` - Revision int64 `json:"revision"` - NextRunAt *string `json:"nextRunAt,omitempty"` - // Newest-first retained run summaries. - Runs []AutomationRunSummary `json:"runs"` - RunsNextCursor *string `json:"runsNextCursor,omitempty"` - Runtime *AutomationRuntimeState `json:"runtime,omitempty"` - Operations []AutomationOperation `json:"operations"` - CreatedAt string `json:"createdAt"` - ModifiedAt string `json:"modifiedAt"` - Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Monotonically increasing definition revision. Clients pass the revision + // they observed as `updateAutomation.expectedRevision`. + Revision int64 `json:"revision"` + // Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + NextRunAt *string `json:"nextRunAt,omitempty"` + // Newest-first retained run summaries. This is a bounded window; use + // `fetchAutomationRuns` when {@link runsNextCursor} is present. + Runs []AutomationRunSummary `json:"runs"` + // Opaque cursor for the next older run-history page. + RunsNextCursor *string `json:"runsNextCursor,omitempty"` + // Optional host-resolved execution context. + Runtime *AutomationRuntimeState `json:"runtime,omitempty"` + // Operations currently permitted for this automation. + Operations []AutomationOperation `json:"operations"` + // Creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` + // Last definition modification timestamp in ISO 8601 format. + ModifiedAt string `json:"modifiedAt"` + // Opaque host-defined state metadata. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } +// Summary of why a run cannot currently make progress. type AutomationRunBlocker struct { + // Category of the outstanding dependency. Kind AutomationRunBlockerKind `json:"kind"` } +// Cause recorded for a client-requested manual run. type AutomationManualRunCause struct { Kind AutomationRunCauseKind `json:"kind"` } +// Cause recorded for a run created by one of the automation's triggers. type AutomationTriggeredRunCause struct { - Kind AutomationRunCauseKind `json:"kind"` - TriggerId string `json:"triggerId"` - ScheduledFor *string `json:"scheduledFor,omitempty"` - CatchUp *bool `json:"catchUp,omitempty"` - // Host-defined event provenance containing no secrets. + Kind AutomationRunCauseKind `json:"kind"` + // Matches the stable {@link AutomationTrigger.id} in the definition. + TriggerId string `json:"triggerId"` + // Intended schedule occurrence as an ISO 8601 timestamp. Present for + // schedule triggers and normally absent for event triggers. + ScheduledFor *string `json:"scheduledFor,omitempty"` + // `true` when this is a catch-up run created by + // {@link AutomationMisfirePolicy.RunOnce}. + CatchUp *bool `json:"catchUp,omitempty"` + // Host-defined, non-secret event provenance suitable for display or audit. + // This is descriptive context, not an input that clients replay. Event map[string]json.RawMessage `json:"event,omitempty"` } +// A durable run exists but has not begun external execution. type AutomationPendingRunLifecycle struct { - Status AutomationRunStatus `json:"status"` - CreatedAt string `json:"createdAt"` + Status AutomationRunStatus `json:"status"` + // Run creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` } +// The run is actively executing linked sessions. type AutomationRunningRunLifecycle struct { - Status AutomationRunStatus `json:"status"` - CreatedAt string `json:"createdAt"` - StartedAt string `json:"startedAt"` + Status AutomationRunStatus `json:"status"` + // Run creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` + // First execution start timestamp in ISO 8601 format. + StartedAt string `json:"startedAt"` } +// The run started but is temporarily unable to progress. type AutomationBlockedRunLifecycle struct { - Status AutomationRunStatus `json:"status"` - CreatedAt string `json:"createdAt"` - StartedAt string `json:"startedAt"` - Blocker AutomationRunBlocker `json:"blocker"` + Status AutomationRunStatus `json:"status"` + // Run creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` + // First execution start timestamp in ISO 8601 format. + StartedAt string `json:"startedAt"` + // Coarse blocker summary; linked sessions contain interaction details. + Blocker AutomationRunBlocker `json:"blocker"` } +// Terminal lifecycle for a successfully completed run. type AutomationCompletedRunLifecycle struct { - Status AutomationRunStatus `json:"status"` - CreatedAt string `json:"createdAt"` - StartedAt string `json:"startedAt"` - CompletedAt string `json:"completedAt"` - Usage *UsageInfo `json:"usage,omitempty"` + Status AutomationRunStatus `json:"status"` + // Run creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` + // First execution start timestamp in ISO 8601 format. + StartedAt string `json:"startedAt"` + // Completion timestamp in ISO 8601 format. + CompletedAt string `json:"completedAt"` + // Optional aggregate model usage across all linked sessions. + Usage *UsageInfo `json:"usage,omitempty"` } +// Terminal lifecycle for a run that ended with an error. +// +// `startedAt` is absent when failure occurred before execution began, such as +// session-template validation or workspace preparation. type AutomationFailedRunLifecycle struct { - Status AutomationRunStatus `json:"status"` - CreatedAt string `json:"createdAt"` - StartedAt *string `json:"startedAt,omitempty"` - CompletedAt string `json:"completedAt"` - Error ErrorInfo `json:"error"` + Status AutomationRunStatus `json:"status"` + // Run creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` + // First execution start timestamp in ISO 8601 format, when execution began. + StartedAt *string `json:"startedAt,omitempty"` + // Failure timestamp in ISO 8601 format. + CompletedAt string `json:"completedAt"` + // Stable machine-readable and human-readable failure information. + Error ErrorInfo `json:"error"` } +// Terminal lifecycle for a cancelled run. +// +// `startedAt` is absent when cancellation completed while the run was still +// pending. type AutomationCancelledRunLifecycle struct { - Status AutomationRunStatus `json:"status"` - CreatedAt string `json:"createdAt"` - StartedAt *string `json:"startedAt,omitempty"` - CompletedAt string `json:"completedAt"` + Status AutomationRunStatus `json:"status"` + // Run creation timestamp in ISO 8601 format. + CreatedAt string `json:"createdAt"` + // First execution start timestamp in ISO 8601 format, when execution began. + StartedAt *string `json:"startedAt,omitempty"` + // Cancellation completion timestamp in ISO 8601 format. + CompletedAt string `json:"completedAt"` } +// Fetchable output produced at run scope rather than by one specific session. +// +// The inherited {@link ContentRef} identifies how the client obtains the +// content. Session-specific edits, transcripts, and tool results remain on +// their session and chat channels. type AutomationRunArtifact struct { // Content URI Uri URI `json:"uri"` @@ -3844,34 +4015,66 @@ type AutomationRunArtifact struct { // Content MIME type ContentType *string `json:"contentType,omitempty"` // Content nonce - Nonce *string `json:"nonce,omitempty"` - Id string `json:"id"` - Label string `json:"label"` - Meta map[string]json.RawMessage `json:"_meta,omitempty"` + Nonce *string `json:"nonce,omitempty"` + // Stable artifact id within this run, used by artifact actions. + Id string `json:"id"` + // Human-readable label suitable for run-history UI. + Label string `json:"label"` + // Opaque host-defined artifact metadata. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } +// Lightweight projection of a run retained in its automation's history. +// +// A summary contains enough information to render run history without +// subscribing to every `ahp-automation-run:` resource. type AutomationRunSummary struct { - Resource URI `json:"resource"` - Automation URI `json:"automation"` - Cause AutomationRunCause `json:"cause"` - Lifecycle AutomationRunLifecycle `json:"lifecycle"` - PrimarySession *URI `json:"primarySession,omitempty"` - SessionCount int64 `json:"sessionCount"` - ArtifactCount *int64 `json:"artifactCount,omitempty"` - Operations []AutomationRunOperation `json:"operations"` - Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // Subscribable `ahp-automation-run:` URI. + Resource URI `json:"resource"` + // Owning `ahp-automation:` URI. + Automation URI `json:"automation"` + // Immutable reason this run was created. + Cause AutomationRunCause `json:"cause"` + // Current or terminal lifecycle snapshot. + Lifecycle AutomationRunLifecycle `json:"lifecycle"` + // Session the host recommends opening first, when one has been selected. + PrimarySession *URI `json:"primarySession,omitempty"` + // Number of linked sessions, including attempts and workers. + SessionCount int64 `json:"sessionCount"` + // Number of run-scoped artifacts, when cheaply available. + ArtifactCount *int64 `json:"artifactCount,omitempty"` + // Operations currently permitted for this run. + Operations []AutomationRunOperation `json:"operations"` + // Opaque host-defined summary metadata. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } +// Authoritative state of one subscribed `ahp-automation-run:` resource. +// +// The run channel owns task-level lifecycle, provenance, linked-session +// membership, artifacts, and cancellation availability. Linked session and +// chat channels remain authoritative for transcripts, tools, confirmations, +// changesets, and per-session lifecycle. type AutomationRunState struct { - Resource URI `json:"resource"` - Automation URI `json:"automation"` - Cause AutomationRunCause `json:"cause"` - Lifecycle AutomationRunLifecycle `json:"lifecycle"` - Sessions []URI `json:"sessions"` - PrimarySession *URI `json:"primarySession,omitempty"` - Artifacts []AutomationRunArtifact `json:"artifacts"` - Operations []AutomationRunOperation `json:"operations"` - Meta map[string]json.RawMessage `json:"_meta,omitempty"` + // URI of this automation-run channel. + Resource URI `json:"resource"` + // Owning `ahp-automation:` URI. + Automation URI `json:"automation"` + // Immutable reason this run was created. + Cause AutomationRunCause `json:"cause"` + // Current or terminal lifecycle. + Lifecycle AutomationRunLifecycle `json:"lifecycle"` + // Ordered, unique session URIs belonging to this run. Entries may represent + // retries, parallel workers, or delegated attempts. + Sessions []URI `json:"sessions"` + // Session the host recommends opening first, when one has been selected. + PrimarySession *URI `json:"primarySession,omitempty"` + // Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}. + Artifacts []AutomationRunArtifact `json:"artifacts"` + // Operations currently permitted for this run. + Operations []AutomationRunOperation `json:"operations"` + // Opaque host-defined run metadata. + Meta map[string]json.RawMessage `json:"_meta,omitempty"` } // ToolInput is raw tool input represented inline or by content reference. @@ -5230,86 +5433,6 @@ func (u SessionOrigin) MarshalJSON() ([]byte, error) { return json.Marshal(object) } -// AutomationSchedule is the calendar schedule for an automation trigger. -type AutomationSchedule struct { - Value isAutomationSchedule -} - -// isAutomationSchedule is the marker interface implemented by every -// concrete variant of AutomationSchedule. -type isAutomationSchedule interface{ isAutomationSchedule() } - -func (*AutomationHourlySchedule) isAutomationSchedule() {} -func (*AutomationDailySchedule) isAutomationSchedule() {} -func (*AutomationWeeklySchedule) isAutomationSchedule() {} -func (*AutomationCronSchedule) isAutomationSchedule() {} - -// UnmarshalJSON decodes the variant indicated by the "kind" discriminator. -func (u *AutomationSchedule) UnmarshalJSON(data []byte) error { - disc, ok, err := readDiscriminator(data, "kind") - if err != nil { - return err - } - if !ok { - return missingDiscriminatorError("AutomationSchedule", "kind") - } - switch disc { - case "hourly": - var value AutomationHourlySchedule - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value - case "daily": - var value AutomationDailySchedule - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value - case "weekly": - var value AutomationWeeklySchedule - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value - case "cron": - var value AutomationCronSchedule - if err := json.Unmarshal(data, &value); err != nil { - return err - } - u.Value = &value - default: - return unknownDiscriminatorError("AutomationSchedule", "kind", disc) - } - return nil -} - -// MarshalJSON encodes the active variant back to JSON. -func (u AutomationSchedule) MarshalJSON() ([]byte, error) { - if u.Value == nil { - return []byte("null"), nil - } - data, err := json.Marshal(u.Value) - if err != nil { - return nil, err - } - var object map[string]json.RawMessage - if err := json.Unmarshal(data, &object); err != nil { - return nil, err - } - switch u.Value.(type) { - case *AutomationHourlySchedule: - object["kind"] = json.RawMessage("\"hourly\"") - case *AutomationDailySchedule: - object["kind"] = json.RawMessage("\"daily\"") - case *AutomationWeeklySchedule: - object["kind"] = json.RawMessage("\"weekly\"") - case *AutomationCronSchedule: - object["kind"] = json.RawMessage("\"cron\"") - } - return json.Marshal(object) -} - // AutomationTrigger is an automatic trigger for an automation. type AutomationTrigger struct { Value isAutomationTrigger 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 aa2b4f7e1..cca2cd2d8 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 @@ -1506,65 +1506,110 @@ data class ResourceWatchChangedAction( @Serializable data class AutomationDefinitionChangedAction( val type: ActionType, + /** + * Complete replacement definition. + */ val definition: AutomationDefinition, + /** + * New monotonic revision. + */ val revision: Long, + /** + * Definition modification timestamp in ISO 8601 format. + */ val modifiedAt: String, + /** + * Earliest known future scheduled occurrence, or omitted to clear it. + */ val nextRunAt: String? = null ) @Serializable data class AutomationRunSummarySetAction( val type: ActionType, + /** + * New or replacement run summary. + */ val run: AutomationRunSummary ) @Serializable data class AutomationRunSummaryRemovedAction( val type: ActionType, + /** + * {@link AutomationRunSummary.resource} to remove. + */ val run: String ) @Serializable data class AutomationRunsLoadedAction( val type: ActionType, + /** + * Older run summaries in newest-first order within this page. + */ val runs: List, + /** + * Opaque cursor for the next older page, or omitted at the end. + */ val nextCursor: String? = null ) @Serializable data class AutomationRunLifecycleChangedAction( val type: ActionType, + /** + * Complete replacement lifecycle. + */ val lifecycle: AutomationRunLifecycle, + /** + * Complete replacement operation list. + */ val operations: List ) @Serializable data class AutomationRunSessionSetAction( val type: ActionType, + /** + * Session URI to append when it is not already linked. + */ val session: String ) @Serializable data class AutomationRunSessionRemovedAction( val type: ActionType, + /** + * Linked session URI to remove. + */ val session: String ) @Serializable data class AutomationRunPrimarySessionChangedAction( val type: ActionType, + /** + * New primary linked session, or omitted to clear the selection. + */ val primarySession: String? = null ) @Serializable data class AutomationRunArtifactSetAction( val type: ActionType, + /** + * New or replacement artifact. + */ val artifact: AutomationRunArtifact ) @Serializable data class AutomationRunArtifactRemovedAction( val type: ActionType, + /** + * {@link AutomationRunArtifact.id} to remove. + */ val artifactId: String ) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index e3f63014b..608cf60d4 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -342,7 +342,8 @@ data class InitializeResult( */ val telemetry: TelemetryCapabilities? = null, /** - * Host automation support. Absence means unsupported. + * Host-owned automation support. Absence means the host does not expose an + * automation catalogue or automation commands. */ val automations: AutomationCapabilities? = null ) @@ -367,16 +368,39 @@ data class ClientCapabilities( @Serializable data class AutomationCapabilities( + /** + * Availability guarantee for automatic trigger execution. + */ val execution: AutomationExecutionCapabilities, + /** + * Present when clients may call `createAutomation`. + */ val create: AutomationCreateCapability? = null, + /** + * Present when definitions may contain schedule triggers. + */ val schedules: AutomationScheduleCapabilities? = null, + /** + * Present when clients may request cancellation on eligible runs. + */ val runCancellation: AutomationRunCancellationCapability? = null, + /** + * Present when clients may call `previewAutomationSchedule`. + */ val schedulePreview: AutomationSchedulePreviewCapability? = null, + /** + * Maximum terminal run summaries retained per automation. Active runs are not + * counted toward the limit. Absence means the retention limit is + * implementation-defined. + */ val runHistoryLimit: Long? = null ) @Serializable data class AutomationExecutionCapabilities( + /** + * How long automatic trigger evaluation remains available. + */ val lifetime: AutomationExecutionLifetime ) @@ -385,13 +409,10 @@ class AutomationCreateCapability @Serializable data class AutomationScheduleCapabilities( - val kinds: List, - val cron: AutomationCronScheduleCapability? = null -) - -@Serializable -data class AutomationCronScheduleCapability( - val dialect: String, + /** + * Smallest permitted interval between consecutive occurrences. Omission + * means no restriction beyond the cron format's one-minute resolution. + */ val minIntervalMinutes: Long? = null ) @@ -1563,6 +1584,9 @@ data class ListAutomationsParams( * unrecognised cursor SHOULD be rejected with an `InvalidParams` error. */ val cursor: String? = null, + /** + * Optional exact filter on {@link AutomationDefinition.enabled}. + */ val enabled: Boolean? = null ) @@ -1574,6 +1598,9 @@ data class ListAutomationsResult( * {@link PaginatedParams.cursor} to fetch the following page. */ val nextCursor: String? = null, + /** + * Automation summaries in host-defined catalogue order. + */ val items: List ) @@ -1589,13 +1616,25 @@ data class ListAutomationTriggerDefinitionsParams( */ @SerialName("_meta") val meta: Map? = null, + /** + * Prospective provider id, or omitted for the host default. + */ val provider: String? = null, + /** + * Prospective ordered working-directory list. + */ val workingDirectories: List? = null, + /** + * Prospective resolved session configuration values. + */ val sessionConfig: Map? = null ) @Serializable data class ListAutomationTriggerDefinitionsResult( + /** + * Available event trigger definitions. + */ val items: List ) @@ -1611,18 +1650,58 @@ data class CreateAutomationParams( */ @SerialName("_meta") val meta: Map? = null, + /** + * Complete initial definition. + */ val definition: AutomationDefinition, + /** + * Optional idempotency identity when importing a legacy definition. + */ @SerialName("import") - val `import`: JsonElement? = null + val `import`: AutomationImportIdentity? = null +) + +@Serializable +data class AutomationImportIdentity( + /** + * Stable namespace identifying the source implementation or store. + */ + val source: String, + /** + * Identifier shared by every item in one import attempt. + */ + val batchId: String, + /** + * Stable source-side identifier for this definition within the batch. + */ + val itemId: String ) @Serializable data class AutomationDefinitionPatch( + /** + * Replacement human-readable title. + */ val title: String? = null, + /** + * Replacement initial user message. + */ val message: Message? = null, + /** + * Replacement session template. + */ val session: AutomationSessionTemplate? = null, + /** + * Replacement automatic-trigger enabled state. + */ val enabled: Boolean? = null, + /** + * Complete replacement trigger list. + */ val triggers: List? = null, + /** + * Complete replacement implementation-defined metadata. + */ @SerialName("_meta") val meta: Map? = null ) @@ -1639,7 +1718,13 @@ data class UpdateAutomationParams( */ @SerialName("_meta") val meta: Map? = null, + /** + * Revision on which the client based {@link changes}. + */ val expectedRevision: Long, + /** + * Editable fields to replace. + */ val changes: AutomationDefinitionPatch ) @@ -1669,11 +1754,19 @@ data class RunAutomationParams( */ @SerialName("_meta") val meta: Map? = null, + /** + * Durable client-generated idempotency key. Retrying with the same key and + * automation MUST return the original run URI rather than create another + * run. + */ val requestId: String ) @Serializable data class RunAutomationResult( + /** + * Subscribable `ahp-automation-run:` URI. + */ val run: String ) @@ -1689,6 +1782,10 @@ data class FetchAutomationRunsParams( */ @SerialName("_meta") val meta: Map? = null, + /** + * Cursor previously received as {@link AutomationState.runsNextCursor}. + * Omit to request the first page not already included by the snapshot. + */ val cursor: String? = null ) @@ -1707,12 +1804,21 @@ data class PreviewAutomationScheduleParams( */ @SerialName("_meta") val meta: Map? = null, + /** + * Portable AHP cron schedule to evaluate. + */ val schedule: AutomationSchedule, + /** + * Requested maximum number of future occurrences; the host MAY cap it. + */ val count: Long? = null ) @Serializable data class PreviewAutomationScheduleResult( + /** + * Ascending ISO 8601 timestamps. + */ val items: List ) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 4cc3de4a3..4a475f657 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -86,19 +86,37 @@ data class SessionSummaryChangedParams( @Serializable data class AutomationAddedParams( + /** + * Root channel URI. + */ val channel: String, + /** + * Complete summary for the newly visible automation. + */ val summary: AutomationSummary ) @Serializable data class AutomationRemovedParams( + /** + * Root channel URI. + */ val channel: String, + /** + * Removed `ahp-automation:` URI. + */ val automation: String ) @Serializable data class AutomationSummaryChangedParams( + /** + * Root channel URI. + */ val channel: String, + /** + * Complete replacement catalogue summary. + */ val summary: AutomationSummary ) 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 0609ab946..cdd1788f4 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 @@ -751,114 +751,202 @@ enum class ResourceChangeType { DELETED } +/** + * Discriminant describing the durable provenance of a session. + */ @Serializable enum class SessionOriginKind { + /** + * The session was created as part of an automation run. + */ @SerialName("automation") AUTOMATION } +/** + * Operations the host currently permits for an automation. + * + * The list on {@link AutomationState.operations} is authoritative and may + * change over time. Clients MUST NOT infer permission from capabilities alone: + * capabilities describe what the host implementation can support, while + * operations describe what is allowed for this particular automation now. + */ @Serializable enum class AutomationOperation { + /** + * Replace editable fields using `updateAutomation`. + */ @SerialName("update") UPDATE, + /** + * Permanently remove the automation using `disposeAutomation`. + */ @SerialName("dispose") DISPOSE, + /** + * Start a manual run using `runAutomation`. + */ @SerialName("run") RUN } +/** + * Availability guarantee for host-owned automatic trigger evaluation. + * + * This describes the authority that owns one automation catalogue. It does not + * prevent a client from connecting to several authorities with different + * lifetimes (for example, one local host and one managed service). + */ @Serializable enum class AutomationExecutionLifetime { + /** + * Automatic triggers are evaluated only while this host process is running. + * Definitions may remain durable across restarts, but occurrences while the + * process is unavailable are handled according to the trigger's + * {@link AutomationMisfirePolicy}. + */ @SerialName("hostLifetime") HOST_LIFETIME, + /** + * Automatic triggers continue to be evaluated independently of connected + * clients and any particular interactive host process. + */ @SerialName("managed") MANAGED } -@Serializable -enum class AutomationScheduleKind { - @SerialName("hourly") - HOURLY, - @SerialName("daily") - DAILY, - @SerialName("weekly") - WEEKLY, - @SerialName("cron") - CRON -} - -@Serializable -enum class AutomationWeekday { - @SerialName("monday") - MONDAY, - @SerialName("tuesday") - TUESDAY, - @SerialName("wednesday") - WEDNESDAY, - @SerialName("thursday") - THURSDAY, - @SerialName("friday") - FRIDAY, - @SerialName("saturday") - SATURDAY, - @SerialName("sunday") - SUNDAY -} - +/** + * How a host handles schedule occurrences missed while automatic execution was + * unavailable. + */ @Serializable enum class AutomationMisfirePolicy { + /** + * Discard missed occurrences and wait for the next future occurrence. + */ @SerialName("skip") SKIP, + /** + * Start at most one catch-up run when execution becomes available, regardless + * of how many occurrences were missed. + */ @SerialName("runOnce") RUN_ONCE } +/** + * Discriminant for automatic trigger definitions. + */ @Serializable enum class AutomationTriggerKind { + /** + * A portable recurring {@link AutomationSchedule}. + */ @SerialName("schedule") SCHEDULE, + /** + * A host-defined external event discovered from trigger definitions. + */ @SerialName("event") EVENT } +/** + * Lifecycle status of one automation run. + * + * `completed`, `failed`, and `cancelled` are terminal. `blocked` is + * non-terminal: the host may return the run to `running` after the linked + * session resolves the blocker. + */ @Serializable enum class AutomationRunStatus { + /** + * The durable run record exists but execution has not started. + */ @SerialName("pending") PENDING, + /** + * One or more linked sessions are actively executing. + */ @SerialName("running") RUNNING, + /** + * Execution is paused on an interaction or client-side dependency. + */ @SerialName("blocked") BLOCKED, + /** + * Execution finished successfully. + */ @SerialName("completed") COMPLETED, + /** + * Execution ended with an error. + */ @SerialName("failed") FAILED, + /** + * Execution ended because cancellation was accepted. + */ @SerialName("cancelled") CANCELLED } +/** + * Coarse reason a run is blocked. + * + * Detailed prompts, confirmations, authentication requests, and tool state + * remain authoritative on linked session and chat channels. + */ @Serializable enum class AutomationRunBlockerKind { + /** + * A linked session is waiting for an answer to a user-input request. + */ @SerialName("userInput") USER_INPUT, + /** + * A linked session is waiting for tool confirmation. + */ @SerialName("toolConfirmation") TOOL_CONFIRMATION, + /** + * Execution requires authentication or renewed credentials. + */ @SerialName("authentication") AUTHENTICATION, + /** + * Work must be performed by or delegated to a connected client. + */ @SerialName("clientExecution") CLIENT_EXECUTION } +/** + * Discriminant describing what created an automation run. + */ @Serializable enum class AutomationRunCauseKind { + /** + * A client explicitly invoked `runAutomation`. + */ @SerialName("manual") MANUAL, + /** + * An automatic schedule or event trigger fired. + */ @SerialName("trigger") TRIGGER } +/** + * Operations the host currently permits for a run. + */ @Serializable enum class AutomationRunOperation { + /** + * Request cancellation with `automationRun/cancelRequested`. + */ @SerialName("cancel") CANCEL } @@ -4786,51 +4874,25 @@ data class ResourceChange( @Serializable data class AutomationSessionOrigin( val kind: SessionOriginKind, - val automation: String, - val run: String -) - -@Serializable -data class AutomationLocalTime( - val hour: Long, - val minute: Long -) - -@Serializable -data class AutomationHourlySchedule( - val kind: AutomationScheduleKind -) - -@Serializable -data class AutomationDailySchedule( - val kind: AutomationScheduleKind, - val time: AutomationLocalTime, /** - * IANA time-zone identifier. + * Owning `ahp-automation:` URI. */ - val timeZone: String -) - -@Serializable -data class AutomationWeeklySchedule( - val kind: AutomationScheduleKind, - val weekday: AutomationWeekday, - val time: AutomationLocalTime, + val automation: String, /** - * IANA time-zone identifier. + * Owning `ahp-automation-run:` URI. */ - val timeZone: String + val run: String ) @Serializable -data class AutomationCronSchedule( - val kind: AutomationScheduleKind, +data class AutomationSchedule( /** - * Standard five-field Unix cron expression. + * Five-field AHP cron expression described by {@link AutomationSchedule}. */ val expression: String, /** - * IANA time-zone identifier. + * IANA Time Zone Database identifier used to interpret the expression, for + * example `"UTC"` or `"Europe/Berlin"`. */ val timeZone: String ) @@ -4838,130 +4900,263 @@ data class AutomationCronSchedule( @Serializable data class AutomationScheduleTrigger( /** - * Stable within the automation definition. + * Identifier unique and stable within this automation definition. Run causes + * refer back to this value. */ val id: String, val kind: AutomationTriggerKind, + /** + * Recurrence and time zone evaluated by the host. + */ val schedule: AutomationSchedule, + /** + * Policy for missed occurrences. Omission is equivalent to + * {@link AutomationMisfirePolicy.RunOnce}. + */ val misfirePolicy: AutomationMisfirePolicy? = null ) @Serializable data class AutomationEventTrigger( /** - * Stable within the automation definition. + * Identifier unique and stable within this automation definition. Run causes + * refer back to this value. */ val id: String, val kind: AutomationTriggerKind, /** - * Stable host-defined trigger type. + * Matches {@link AutomationTriggerDefinition.type}. */ val type: String, /** - * Selected event actions. + * Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + * trigger type. */ val events: List, /** - * Schema-defined values. Unknown entries must survive round-trips. + * Values described by {@link AutomationTriggerDefinition.configSchema}. + * Clients MUST preserve unknown entries when editing other fields. */ val config: Map? = null ) @Serializable data class AutomationTriggerEventDefinition( + /** + * Stable event id stored in {@link AutomationEventTrigger.events}. + */ val id: String, + /** + * Human-readable label suitable for selection UI. + */ val title: String, + /** + * Optional longer explanation of when this event fires. + */ val description: String? = null ) @Serializable data class AutomationTriggerDefinition( + /** + * Stable type id stored in {@link AutomationEventTrigger.type}. + */ val type: String, + /** + * Human-readable trigger type name. + */ val title: String, + /** + * Optional longer explanation of the trigger source. + */ val description: String? = null, + /** + * Events clients may select for this trigger type. + */ val events: List, + /** + * Optional schema for {@link AutomationEventTrigger.config}. + */ val configSchema: ConfigSchema? = null ) @Serializable data class AutomationSessionTemplate( + /** + * Provider id. Omit to use the host's default provider. + */ val provider: String? = null, + /** + * Optional model selection resolved when a run starts. + */ val model: ModelSelection? = null, + /** + * Optional custom agent selection resolved when a run starts. + */ val agent: AgentSelection? = null, /** - * Absence means a workspace-less session. + * Ordered working-directory URIs for each created session. Absence means a + * workspace-less session. */ val workingDirectories: List? = null, /** - * Values resolved through `resolveSessionConfig`. + * Session configuration values accepted by `createSession`, normally + * obtained from `resolveSessionConfig`. */ val config: Map? = null ) @Serializable data class AutomationDefinition( + /** + * Human-readable automation name. + */ val title: String, /** - * Initial user message sent to each new session. + * Initial message sent to every newly created run session. Its origin MUST be + * `user`. */ val message: Message, + /** + * Template used to create fresh sessions for each run. + */ val session: AutomationSessionTemplate, /** - * Controls automatic triggers; manual runs remain permitted. + * Whether automatic triggers may create runs. Manual runs remain available + * whenever {@link AutomationOperation.Run} is advertised. */ val enabled: Boolean, /** - * Empty means manual-only. + * Automatic triggers. An empty list means manual-only. */ val triggers: List, + /** + * Opaque implementation-defined metadata. Clients MUST preserve unknown + * entries when updating the definition. + */ @SerialName("_meta") val meta: Map? = null ) @Serializable data class AutomationRuntimeState( + /** + * Effective working directories after host-side preparation, such as + * materializing a managed workspace. + */ val workingDirectories: List? = null, + /** + * Opaque host-defined runtime metadata. + */ @SerialName("_meta") val meta: Map? = null ) @Serializable data class AutomationSummary( + /** + * Subscribable `ahp-automation:` URI. + */ val resource: String, + /** + * Current {@link AutomationDefinition.title}. + */ val title: String, + /** + * Current {@link AutomationDefinition.enabled} value. + */ val enabled: Boolean, + /** + * Number of automatic triggers in the current definition. + */ val triggerCount: Long, + /** + * Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + */ val nextRunAt: String? = null, + /** + * Most recent retained run, when any run exists. + */ val lastRun: AutomationRunSummary? = null, + /** + * Monotonic definition revision used for optimistic concurrency. + */ val revision: Long, + /** + * Operations currently permitted for this automation. + */ val operations: List, + /** + * Creation timestamp in ISO 8601 format. + */ val createdAt: String, + /** + * Last definition modification timestamp in ISO 8601 format. + */ val modifiedAt: String, + /** + * Opaque host-defined catalogue metadata. + */ @SerialName("_meta") val meta: Map? = null ) @Serializable data class AutomationState( + /** + * URI of this automation channel. + */ val resource: String, + /** + * Current durable definition. + */ val definition: AutomationDefinition, + /** + * Monotonically increasing definition revision. Clients pass the revision + * they observed as `updateAutomation.expectedRevision`. + */ val revision: Long, + /** + * Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + */ val nextRunAt: String? = null, /** - * Newest-first retained run summaries. + * Newest-first retained run summaries. This is a bounded window; use + * `fetchAutomationRuns` when {@link runsNextCursor} is present. */ val runs: List, + /** + * Opaque cursor for the next older run-history page. + */ val runsNextCursor: String? = null, + /** + * Optional host-resolved execution context. + */ val runtime: AutomationRuntimeState? = null, + /** + * Operations currently permitted for this automation. + */ val operations: List, + /** + * Creation timestamp in ISO 8601 format. + */ val createdAt: String, + /** + * Last definition modification timestamp in ISO 8601 format. + */ val modifiedAt: String, + /** + * Opaque host-defined state metadata. + */ @SerialName("_meta") val meta: Map? = null ) @Serializable data class AutomationRunBlocker( + /** + * Category of the outstanding dependency. + */ val kind: AutomationRunBlockerKind ) @@ -4973,11 +5168,23 @@ data class AutomationManualRunCause( @Serializable data class AutomationTriggeredRunCause( val kind: AutomationRunCauseKind, + /** + * Matches the stable {@link AutomationTrigger.id} in the definition. + */ val triggerId: String, + /** + * Intended schedule occurrence as an ISO 8601 timestamp. Present for + * schedule triggers and normally absent for event triggers. + */ val scheduledFor: String? = null, + /** + * `true` when this is a catch-up run created by + * {@link AutomationMisfirePolicy.RunOnce}. + */ val catchUp: Boolean? = null, /** - * Host-defined event provenance containing no secrets. + * Host-defined, non-secret event provenance suitable for display or audit. + * This is descriptive context, not an input that clients replay. */ val event: Map? = null ) @@ -4985,47 +5192,98 @@ data class AutomationTriggeredRunCause( @Serializable data class AutomationPendingRunLifecycle( val status: AutomationRunStatus, + /** + * Run creation timestamp in ISO 8601 format. + */ val createdAt: String ) @Serializable data class AutomationRunningRunLifecycle( val status: AutomationRunStatus, + /** + * Run creation timestamp in ISO 8601 format. + */ val createdAt: String, + /** + * First execution start timestamp in ISO 8601 format. + */ val startedAt: String ) @Serializable data class AutomationBlockedRunLifecycle( val status: AutomationRunStatus, + /** + * Run creation timestamp in ISO 8601 format. + */ val createdAt: String, + /** + * First execution start timestamp in ISO 8601 format. + */ val startedAt: String, + /** + * Coarse blocker summary; linked sessions contain interaction details. + */ val blocker: AutomationRunBlocker ) @Serializable data class AutomationCompletedRunLifecycle( val status: AutomationRunStatus, + /** + * Run creation timestamp in ISO 8601 format. + */ val createdAt: String, + /** + * First execution start timestamp in ISO 8601 format. + */ val startedAt: String, + /** + * Completion timestamp in ISO 8601 format. + */ val completedAt: String, + /** + * Optional aggregate model usage across all linked sessions. + */ val usage: UsageInfo? = null ) @Serializable data class AutomationFailedRunLifecycle( val status: AutomationRunStatus, + /** + * Run creation timestamp in ISO 8601 format. + */ val createdAt: String, + /** + * First execution start timestamp in ISO 8601 format, when execution began. + */ val startedAt: String? = null, + /** + * Failure timestamp in ISO 8601 format. + */ val completedAt: String, + /** + * Stable machine-readable and human-readable failure information. + */ val error: ErrorInfo ) @Serializable data class AutomationCancelledRunLifecycle( val status: AutomationRunStatus, + /** + * Run creation timestamp in ISO 8601 format. + */ val createdAt: String, + /** + * First execution start timestamp in ISO 8601 format, when execution began. + */ val startedAt: String? = null, + /** + * Cancellation completion timestamp in ISO 8601 format. + */ val completedAt: String ) @@ -5047,36 +5305,100 @@ data class AutomationRunArtifact( * Content nonce */ val nonce: String? = null, + /** + * Stable artifact id within this run, used by artifact actions. + */ val id: String, + /** + * Human-readable label suitable for run-history UI. + */ val label: String, + /** + * Opaque host-defined artifact metadata. + */ @SerialName("_meta") val meta: Map? = null ) @Serializable data class AutomationRunSummary( + /** + * Subscribable `ahp-automation-run:` URI. + */ val resource: String, + /** + * Owning `ahp-automation:` URI. + */ val automation: String, + /** + * Immutable reason this run was created. + */ val cause: AutomationRunCause, + /** + * Current or terminal lifecycle snapshot. + */ val lifecycle: AutomationRunLifecycle, + /** + * Session the host recommends opening first, when one has been selected. + */ val primarySession: String? = null, + /** + * Number of linked sessions, including attempts and workers. + */ val sessionCount: Long, + /** + * Number of run-scoped artifacts, when cheaply available. + */ val artifactCount: Long? = null, + /** + * Operations currently permitted for this run. + */ val operations: List, + /** + * Opaque host-defined summary metadata. + */ @SerialName("_meta") val meta: Map? = null ) @Serializable data class AutomationRunState( + /** + * URI of this automation-run channel. + */ val resource: String, + /** + * Owning `ahp-automation:` URI. + */ val automation: String, + /** + * Immutable reason this run was created. + */ val cause: AutomationRunCause, + /** + * Current or terminal lifecycle. + */ val lifecycle: AutomationRunLifecycle, + /** + * Ordered, unique session URIs belonging to this run. Entries may represent + * retries, parallel workers, or delegated attempts. + */ val sessions: List, + /** + * Session the host recommends opening first, when one has been selected. + */ val primarySession: String? = null, + /** + * Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}. + */ val artifacts: List, + /** + * Operations currently permitted for this run. + */ val operations: List, + /** + * Opaque host-defined run metadata. + */ @SerialName("_meta") val meta: Map? = null ) @@ -6134,60 +6456,6 @@ internal object SessionOriginSerializer : KSerializer { } } -@Serializable(with = AutomationScheduleSerializer::class) -sealed interface AutomationSchedule - -@JvmInline -value class AutomationScheduleHourly(val value: AutomationHourlySchedule) : AutomationSchedule -@JvmInline -value class AutomationScheduleDaily(val value: AutomationDailySchedule) : AutomationSchedule -@JvmInline -value class AutomationScheduleWeekly(val value: AutomationWeeklySchedule) : AutomationSchedule -@JvmInline -value class AutomationScheduleCron(val value: AutomationCronSchedule) : AutomationSchedule - -internal object AutomationScheduleSerializer : KSerializer { - override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("AutomationSchedule") - - override fun deserialize(decoder: Decoder): AutomationSchedule { - val input = decoder as? JsonDecoder - ?: error("AutomationSchedule can only be deserialized from JSON") - val element = input.decodeJsonElement() - val obj = element as? JsonObject - ?: error("Expected JsonObject for AutomationSchedule") - val discriminant = (obj["kind"] as? JsonPrimitive)?.content - ?: error("Missing kind discriminator on AutomationSchedule") - return when (discriminant) { - "hourly" -> AutomationScheduleHourly(input.json.decodeFromJsonElement(AutomationHourlySchedule.serializer(), element)) - "daily" -> AutomationScheduleDaily(input.json.decodeFromJsonElement(AutomationDailySchedule.serializer(), element)) - "weekly" -> AutomationScheduleWeekly(input.json.decodeFromJsonElement(AutomationWeeklySchedule.serializer(), element)) - "cron" -> AutomationScheduleCron(input.json.decodeFromJsonElement(AutomationCronSchedule.serializer(), element)) - else -> error("Unknown AutomationSchedule discriminator: $discriminant") - } - } - - override fun serialize(encoder: Encoder, value: AutomationSchedule) { - val output = encoder as? JsonEncoder - ?: error("AutomationSchedule can only be serialized to JSON") - val element: JsonElement = when (value) { - is AutomationScheduleHourly -> output.json.encodeToJsonElement(AutomationHourlySchedule.serializer(), value.value) - is AutomationScheduleDaily -> output.json.encodeToJsonElement(AutomationDailySchedule.serializer(), value.value) - is AutomationScheduleWeekly -> output.json.encodeToJsonElement(AutomationWeeklySchedule.serializer(), value.value) - is AutomationScheduleCron -> output.json.encodeToJsonElement(AutomationCronSchedule.serializer(), value.value) - } - val encodedObject = element.jsonObject.toMutableMap() - val discriminant = when (value) { - is AutomationScheduleHourly -> "hourly" - is AutomationScheduleDaily -> "daily" - is AutomationScheduleWeekly -> "weekly" - is AutomationScheduleCron -> "cron" - } - if (discriminant != null) encodedObject["kind"] = JsonPrimitive(discriminant) - output.encodeJsonElement(JsonObject(encodedObject)) - } -} - @Serializable(with = AutomationTriggerSerializer::class) sealed interface AutomationTrigger diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt index 432fe6ae5..fbe2e4c48 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/DiscriminatedUnionTest.kt @@ -1,10 +1,5 @@ package com.microsoft.agenthostprotocol -import com.microsoft.agenthostprotocol.generated.AutomationDailySchedule -import com.microsoft.agenthostprotocol.generated.AutomationLocalTime -import com.microsoft.agenthostprotocol.generated.AutomationSchedule -import com.microsoft.agenthostprotocol.generated.AutomationScheduleDaily -import com.microsoft.agenthostprotocol.generated.AutomationScheduleKind import com.microsoft.agenthostprotocol.generated.ChangesetOperationRangeTarget import com.microsoft.agenthostprotocol.generated.ChangesetOperationResourceTarget import com.microsoft.agenthostprotocol.generated.ChangesetOperationTarget @@ -87,20 +82,6 @@ class DiscriminatedUnionTest { assertIs(decoded) } - @Test - fun `AutomationSchedule serializer uses wrapper discriminant`() { - val schedule: AutomationSchedule = AutomationScheduleDaily( - AutomationDailySchedule( - kind = AutomationScheduleKind.HOURLY, - time = AutomationLocalTime(hour = 9, minute = 30), - timeZone = "Europe/Berlin", - ), - ) - - val encoded = json.encodeToString(AutomationSchedule.serializer(), schedule) - assertEquals(JsonPrimitive("daily"), json.parseToJsonElement(encoded).jsonObject["kind"]) - } - @Test fun `ChatInputQuestion accepts both number and integer wire kinds`() { // Both "number" and "integer" wire values map to the same Kotlin diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index b810c25e5..60f567c97 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1759,74 +1759,134 @@ pub struct ResourceWatchChangedAction { pub changes: AnyValue, } +/// Replace the editable definition after a successful `updateAutomation` or +/// another host-authorized definition change. +/// +/// Full replacement semantics apply to `definition`. The reducer also replaces +/// the revision and modification timestamp. Omitting `nextRunAt` clears the +/// previously projected next occurrence. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationDefinitionChangedAction { + /// Complete replacement definition. pub definition: AutomationDefinition, + /// New monotonic revision. pub revision: i64, + /// Definition modification timestamp in ISO 8601 format. pub modified_at: String, + /// Earliest known future scheduled occurrence, or omitted to clear it. #[serde(default, skip_serializing_if = "Option::is_none")] pub next_run_at: Option, } +/// Upsert one run summary in the retained history. +/// +/// Existing entries are replaced by {@link AutomationRunSummary.resource}. A +/// previously unseen run is inserted at the front because history is +/// newest-first. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunSummarySetAction { + /// New or replacement run summary. pub run: AutomationRunSummary, } +/// Remove one retained run summary by its automation-run URI. +/// +/// The action is a no-op when the URI is not present in the current history +/// window. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunSummaryRemovedAction { + /// {@link AutomationRunSummary.resource} to remove. pub run: Uri, } +/// Append an older page of run summaries returned by +/// `fetchAutomationRuns`. +/// +/// Entries already present by resource URI are ignored, preserving the +/// newest-first ordering of the existing history followed by the fetched page. +/// Omitting `nextCursor` marks the end of retained history. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunsLoadedAction { + /// Older run summaries in newest-first order within this page. pub runs: Vec, + /// Opaque cursor for the next older page, or omitted at the end. #[serde(default, skip_serializing_if = "Option::is_none")] pub next_cursor: Option, } +/// Replace the run lifecycle and currently allowed operations atomically. +/// +/// The host dispatches this action for every lifecycle transition. Terminal +/// lifecycles normally carry an empty operations list. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunLifecycleChangedAction { + /// Complete replacement lifecycle. pub lifecycle: AutomationRunLifecycle, + /// Complete replacement operation list. pub operations: Vec, } +/// Add a session to the run's ordered session catalogue. +/// +/// Session URIs are unique. Setting an existing URI is a no-op. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunSessionSetAction { + /// Session URI to append when it is not already linked. pub session: Uri, } +/// Remove a linked session from the run. +/// +/// Removing the current primary session also clears +/// {@link AutomationRunState.primarySession}. An unknown URI is a no-op. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunSessionRemovedAction { + /// Linked session URI to remove. pub session: Uri, } +/// Select or clear the session clients should open first for this run. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AutomationRunPrimarySessionChangedAction { + /// New primary linked session, or omitted to clear the selection. #[serde(default, skip_serializing_if = "Option::is_none")] pub primary_session: Option, } +/// Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunArtifactSetAction { + /// New or replacement artifact. pub artifact: AutomationRunArtifact, } +/// Remove a run-scoped artifact by id. +/// +/// The action is a no-op when the id is not present. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunArtifactRemovedAction { + /// {@link AutomationRunArtifact.id} to remove. pub artifact_id: String, } +/// Ask the host to cancel this run. +/// +/// This is the only client-dispatchable automation-run action. It is a +/// side-effect request and deliberately leaves optimistic state unchanged. The +/// authoritative outcome arrives later through +/// {@link AutomationRunLifecycleChangedAction}: cancellation may transition to +/// `cancelled`, or the run may complete or fail before cancellation takes +/// effect. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunCancelRequestedAction {} diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index d5d147f31..8c5a3b3e3 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -16,10 +16,10 @@ use crate::actions::{ActionEnvelope, StateAction}; #[allow(unused_imports)] use crate::state::{ AgentSelection, AutomationDefinition, AutomationExecutionLifetime, AutomationSchedule, - AutomationScheduleKind, AutomationSessionTemplate, AutomationSummary, AutomationTrigger, - AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, - SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, - SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn, + AutomationSessionTemplate, AutomationSummary, AutomationTrigger, AutomationTriggerDefinition, + ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, + SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, + TelemetryCapabilities, TerminalClaim, TextRange, Turn, }; // ─── Enums ──────────────────────────────────────────────────────────── @@ -197,7 +197,8 @@ pub struct InitializeResult { /// filtering). Clients MAY ignore signals they cannot process. #[serde(default, skip_serializing_if = "Option::is_none")] pub telemetry: Option, - /// Host automation support. Absence means unsupported. + /// Host-owned automation support. Absence means the host does not expose an + /// automation catalogue or automation commands. #[serde(default, skip_serializing_if = "Option::is_none")] pub automations: Option, } @@ -225,52 +226,77 @@ pub struct ClientCapabilities { pub mcp_apps: Option, } +/// Automation features supported by this host authority. +/// +/// Capabilities describe implementation support. Per-resource +/// {@link AutomationState.operations} and +/// {@link AutomationRunState.operations} remain authoritative for whether a +/// particular operation is currently allowed. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationCapabilities { + /// Availability guarantee for automatic trigger execution. pub execution: AutomationExecutionCapabilities, + /// Present when clients may call `createAutomation`. #[serde(default, skip_serializing_if = "Option::is_none")] pub create: Option, + /// Present when definitions may contain schedule triggers. #[serde(default, skip_serializing_if = "Option::is_none")] pub schedules: Option, + /// Present when clients may request cancellation on eligible runs. #[serde(default, skip_serializing_if = "Option::is_none")] pub run_cancellation: Option, + /// Present when clients may call `previewAutomationSchedule`. #[serde(default, skip_serializing_if = "Option::is_none")] pub schedule_preview: Option, + /// Maximum terminal run summaries retained per automation. Active runs are not + /// counted toward the limit. Absence means the retention limit is + /// implementation-defined. #[serde(default, skip_serializing_if = "Option::is_none")] pub run_history_limit: Option, } +/// Automatic trigger execution availability. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationExecutionCapabilities { + /// How long automatic trigger evaluation remains available. pub lifetime: AutomationExecutionLifetime, } +/// Presence capability for `createAutomation`. +/// +/// The empty object means "supported"; fields are reserved for future +/// create-specific options. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationCreateCapability {} -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +/// Host restrictions on portable {@link AutomationSchedule} triggers. +/// +/// The cron grammar itself is fixed by AHP. Hosts MUST accept every expression +/// in that grammar unless it violates an advertised interval restriction. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AutomationScheduleCapabilities { - pub kinds: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cron: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AutomationCronScheduleCapability { - pub dialect: String, + /// Smallest permitted interval between consecutive occurrences. Omission + /// means no restriction beyond the cron format's one-minute resolution. #[serde(default, skip_serializing_if = "Option::is_none")] pub min_interval_minutes: Option, } +/// Presence capability for `automationRun/cancelRequested`. +/// +/// The empty object means "supported"; clients must additionally check for +/// {@link AutomationRunOperation.Cancel} on each run. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunCancellationCapability {} +/// Presence capability for `previewAutomationSchedule`. +/// +/// The empty object means "supported"; fields are reserved for future preview +/// limits or options. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationSchedulePreviewCapability {} @@ -1483,6 +1509,12 @@ pub struct ChangesetOperationFollowUp { pub external: Option, } +/// List the host's automation catalogue without subscribing to every +/// automation channel. +/// +/// Results are lightweight {@link AutomationSummary} entries. Clients SHOULD +/// re-run this command after reconnect because root catalogue notifications are +/// not replayed. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListAutomationsParams { @@ -1503,10 +1535,12 @@ pub struct ListAutomationsParams { /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, + /// Optional exact filter on {@link AutomationDefinition.enabled}. #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, } +/// One page of the automation catalogue. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListAutomationsResult { @@ -1515,9 +1549,15 @@ pub struct ListAutomationsResult { /// {@link PaginatedParams.cursor} to fetch the following page. #[serde(default, skip_serializing_if = "Option::is_none")] pub next_cursor: Option, + /// Automation summaries in host-defined catalogue order. pub items: Vec, } +/// Discover event-trigger types available for a prospective session template. +/// +/// Hosts may vary definitions by provider, workspace, and session +/// configuration. Schedule triggers are protocol-defined and therefore do not +/// appear in this result. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListAutomationTriggerDefinitionsParams { @@ -1527,20 +1567,31 @@ pub struct ListAutomationTriggerDefinitionsParams { /// Receivers MUST ignore keys they do not understand. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, + /// Prospective provider id, or omitted for the host default. #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, + /// Prospective ordered working-directory list. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// Prospective resolved session configuration values. #[serde(default, skip_serializing_if = "Option::is_none")] pub session_config: Option, } +/// Host-defined event trigger types available for the supplied context. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListAutomationTriggerDefinitionsResult { + /// Available event trigger definitions. pub items: Vec, } +/// Create a durable automation at a client-chosen URI. +/// +/// `channel` MUST use the `ahp-automation:` scheme and MUST NOT already identify +/// an unrelated automation. The host validates the complete definition, +/// persists it, and makes it visible through the root catalogue before +/// returning success. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateAutomationParams { @@ -1550,28 +1601,62 @@ pub struct CreateAutomationParams { /// Receivers MUST ignore keys they do not understand. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, + /// Complete initial definition. pub definition: AutomationDefinition, + /// Optional idempotency identity when importing a legacy definition. #[serde(default, skip_serializing_if = "Option::is_none")] - pub import: Option, + pub import: Option, +} + +/// Stable source identity used to make legacy automation import idempotent. +/// +/// The host remembers this identity independently of the client-chosen +/// automation URI. Retrying an interrupted migration with the same values MUST +/// resolve to the previously imported item rather than creating a duplicate. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationImportIdentity { + /// Stable namespace identifying the source implementation or store. + pub source: String, + /// Identifier shared by every item in one import attempt. + pub batch_id: String, + /// Stable source-side identifier for this definition within the batch. + pub item_id: String, } +/// Partial replacement of editable {@link AutomationDefinition} fields. +/// +/// Omitted fields are unchanged. Supplied arrays and objects replace their +/// corresponding values in full; they are not merged recursively. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AutomationDefinitionPatch { + /// Replacement human-readable title. #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, + /// Replacement initial user message. #[serde(default, skip_serializing_if = "Option::is_none")] pub message: Option, + /// Replacement session template. #[serde(default, skip_serializing_if = "Option::is_none")] pub session: Option, + /// Replacement automatic-trigger enabled state. #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, + /// Complete replacement trigger list. #[serde(default, skip_serializing_if = "Option::is_none")] pub triggers: Option>, + /// Complete replacement implementation-defined metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } +/// Update editable fields of an existing automation using optimistic +/// concurrency. +/// +/// The host accepts the patch only when `expectedRevision` equals the current +/// {@link AutomationState.revision}. A stale revision is rejected; clients +/// SHOULD reconcile the latest state before retrying. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateAutomationParams { @@ -1581,10 +1666,17 @@ pub struct UpdateAutomationParams { /// Receivers MUST ignore keys they do not understand. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, + /// Revision on which the client based {@link changes}. pub expected_revision: i64, + /// Editable fields to replace. pub changes: AutomationDefinitionPatch, } +/// Permanently remove an automation. +/// +/// The target is supplied by {@link BaseParams.channel}. The host rejects the +/// command when {@link AutomationOperation.Dispose} is not currently +/// advertised, for example while a non-terminal run prevents disposal. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DisposeAutomationParams { @@ -1596,6 +1688,10 @@ pub struct DisposeAutomationParams { pub meta: Option, } +/// Start a manual run of an automation. +/// +/// Manual execution is independent of {@link AutomationDefinition.enabled}. +/// The host persists the run before beginning session side effects. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RunAutomationParams { @@ -1605,15 +1701,25 @@ pub struct RunAutomationParams { /// Receivers MUST ignore keys they do not understand. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, + /// Durable client-generated idempotency key. Retrying with the same key and + /// automation MUST return the original run URI rather than create another + /// run. pub request_id: String, } +/// Result identifying the existing or newly created run. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RunAutomationResult { + /// Subscribable `ahp-automation-run:` URI. pub run: Uri, } +/// Load one older page into the subscribed automation's run-history state. +/// +/// The response only acknowledges the request. Loaded entries arrive through +/// `automation/runsLoaded`, keeping all subscribers synchronized through the +/// normal action stream. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FetchAutomationRunsParams { @@ -1623,14 +1729,22 @@ pub struct FetchAutomationRunsParams { /// Receivers MUST ignore keys they do not understand. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, + /// Cursor previously received as {@link AutomationState.runsNextCursor}. + /// Omit to request the first page not already included by the snapshot. #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } +/// Empty acknowledgement; run summaries are delivered by action. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FetchAutomationRunsResult {} +/// Ask the host to evaluate a schedule without creating an automation. +/// +/// Clients SHOULD use this command for validation and preview instead of +/// implementing their own cron evaluator, especially around time-zone +/// transitions. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PreviewAutomationScheduleParams { @@ -1640,14 +1754,18 @@ pub struct PreviewAutomationScheduleParams { /// Receivers MUST ignore keys they do not understand. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, + /// Portable AHP cron schedule to evaluate. pub schedule: AutomationSchedule, + /// Requested maximum number of future occurrences; the host MAY cap it. #[serde(default, skip_serializing_if = "Option::is_none")] pub count: Option, } +/// Host-canonical future schedule occurrences. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PreviewAutomationScheduleResult { + /// Ascending ISO 8601 timestamps. pub items: Vec, } diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 6d67cebb3..4856b43ae 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -97,24 +97,39 @@ pub struct SessionSummaryChangedParams { pub changes: PartialSessionSummary, } +/// Announces a newly visible automation catalogue entry. +/// +/// Root notifications are live signals and are not replayed after reconnect. +/// Clients that reconnect MUST refresh the catalogue with `listAutomations`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationAddedParams { + /// Root channel URI. pub channel: Uri, + /// Complete summary for the newly visible automation. pub summary: AutomationSummary, } +/// Announces that an automation is no longer present in the root catalogue. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRemovedParams { + /// Root channel URI. pub channel: Uri, + /// Removed `ahp-automation:` URI. pub automation: Uri, } +/// Replaces the root-catalogue summary for an existing automation. +/// +/// Full replacement semantics apply to `summary`; this is not a patch. The +/// corresponding subscribed automation channel remains authoritative. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationSummaryChangedParams { + /// Root channel URI. pub channel: Uri, + /// Complete replacement catalogue summary. pub summary: AutomationSummary, } diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 5f4858591..328927d36 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -587,114 +587,138 @@ pub enum ResourceChangeType { Deleted, } +/// Discriminant describing the durable provenance of a session. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SessionOriginKind { + /// The session was created as part of an automation run. #[serde(rename = "automation")] Automation, } +/// Operations the host currently permits for an automation. +/// +/// The list on {@link AutomationState.operations} is authoritative and may +/// change over time. Clients MUST NOT infer permission from capabilities alone: +/// capabilities describe what the host implementation can support, while +/// operations describe what is allowed for this particular automation now. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationOperation { + /// Replace editable fields using `updateAutomation`. #[serde(rename = "update")] Update, + /// Permanently remove the automation using `disposeAutomation`. #[serde(rename = "dispose")] Dispose, + /// Start a manual run using `runAutomation`. #[serde(rename = "run")] Run, } +/// Availability guarantee for host-owned automatic trigger evaluation. +/// +/// This describes the authority that owns one automation catalogue. It does not +/// prevent a client from connecting to several authorities with different +/// lifetimes (for example, one local host and one managed service). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationExecutionLifetime { + /// Automatic triggers are evaluated only while this host process is running. + /// Definitions may remain durable across restarts, but occurrences while the + /// process is unavailable are handled according to the trigger's + /// {@link AutomationMisfirePolicy}. #[serde(rename = "hostLifetime")] HostLifetime, + /// Automatic triggers continue to be evaluated independently of connected + /// clients and any particular interactive host process. #[serde(rename = "managed")] Managed, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum AutomationScheduleKind { - #[serde(rename = "hourly")] - Hourly, - #[serde(rename = "daily")] - Daily, - #[serde(rename = "weekly")] - Weekly, - #[serde(rename = "cron")] - Cron, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum AutomationWeekday { - #[serde(rename = "monday")] - Monday, - #[serde(rename = "tuesday")] - Tuesday, - #[serde(rename = "wednesday")] - Wednesday, - #[serde(rename = "thursday")] - Thursday, - #[serde(rename = "friday")] - Friday, - #[serde(rename = "saturday")] - Saturday, - #[serde(rename = "sunday")] - Sunday, -} - +/// How a host handles schedule occurrences missed while automatic execution was +/// unavailable. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationMisfirePolicy { + /// Discard missed occurrences and wait for the next future occurrence. #[serde(rename = "skip")] Skip, + /// Start at most one catch-up run when execution becomes available, regardless + /// of how many occurrences were missed. #[serde(rename = "runOnce")] RunOnce, } +/// Discriminant for automatic trigger definitions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationTriggerKind { + /// A portable recurring {@link AutomationSchedule}. #[serde(rename = "schedule")] Schedule, + /// A host-defined external event discovered from trigger definitions. #[serde(rename = "event")] Event, } +/// Lifecycle status of one automation run. +/// +/// `completed`, `failed`, and `cancelled` are terminal. `blocked` is +/// non-terminal: the host may return the run to `running` after the linked +/// session resolves the blocker. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationRunStatus { + /// The durable run record exists but execution has not started. #[serde(rename = "pending")] Pending, + /// One or more linked sessions are actively executing. #[serde(rename = "running")] Running, + /// Execution is paused on an interaction or client-side dependency. #[serde(rename = "blocked")] Blocked, + /// Execution finished successfully. #[serde(rename = "completed")] Completed, + /// Execution ended with an error. #[serde(rename = "failed")] Failed, + /// Execution ended because cancellation was accepted. #[serde(rename = "cancelled")] Cancelled, } +/// Coarse reason a run is blocked. +/// +/// Detailed prompts, confirmations, authentication requests, and tool state +/// remain authoritative on linked session and chat channels. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationRunBlockerKind { + /// A linked session is waiting for an answer to a user-input request. #[serde(rename = "userInput")] UserInput, + /// A linked session is waiting for tool confirmation. #[serde(rename = "toolConfirmation")] ToolConfirmation, + /// Execution requires authentication or renewed credentials. #[serde(rename = "authentication")] Authentication, + /// Work must be performed by or delegated to a connected client. #[serde(rename = "clientExecution")] ClientExecution, } +/// Discriminant describing what created an automation run. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationRunCauseKind { + /// A client explicitly invoked `runAutomation`. #[serde(rename = "manual")] Manual, + /// An automatic schedule or event trigger fired. #[serde(rename = "trigger")] Trigger, } +/// Operations the host currently permits for a run. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AutomationRunOperation { + /// Request cancellation with `automationRun/cancelRequested`. #[serde(rename = "cancel")] Cancel, } @@ -4376,249 +4400,384 @@ pub struct ResourceChange { pub r#type: ResourceChangeType, } +/// Provenance recorded on a session created for an automation run. +/// +/// The links let clients navigate from an ordinary session to the task-level +/// run and its durable definition. The session channel remains authoritative +/// for this session's transcript, tools, confirmations, and changes. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationSessionOrigin { + /// Owning `ahp-automation:` URI. pub automation: Uri, + /// Owning `ahp-automation-run:` URI. pub run: Uri, } +/// A portable recurring schedule evaluated in a named time zone. +/// +/// The expression uses exactly five whitespace-separated fields, in this +/// order: +/// +/// | Field | Values | +/// | --- | --- | +/// | minute | `0`–`59` | +/// | hour | `0`–`23` | +/// | day of month | `1`–`31` | +/// | month | `1`–`12` or `JAN`–`DEC` | +/// | day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday | +/// +/// Month and weekday names are ASCII and case-insensitive. Each field accepts +/// `*`, a single value, an inclusive range (`1-5`), a comma-separated list of +/// values or ranges (`1,3,8-10`), or a step applied to `*` or a range (for +/// example, */15 or `1-30/2`). A step MUST be a positive integer. AHP does +/// not support seconds, years, macros such as `@daily`, or Quartz extensions +/// such as `?`, `L`, `W`, and `#`. +/// +/// Minute, hour, and month must all match. When both day-of-month and +/// day-of-week are restricted (not `*`), an occurrence matches when either day +/// field matches, following Unix cron semantics. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AutomationLocalTime { - pub hour: i64, - pub minute: i64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AutomationHourlySchedule {} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AutomationDailySchedule { - pub time: AutomationLocalTime, - /// IANA time-zone identifier. - pub time_zone: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AutomationWeeklySchedule { - pub weekday: AutomationWeekday, - pub time: AutomationLocalTime, - /// IANA time-zone identifier. - pub time_zone: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AutomationCronSchedule { - /// Standard five-field Unix cron expression. +pub struct AutomationSchedule { + /// Five-field AHP cron expression described by {@link AutomationSchedule}. pub expression: String, - /// IANA time-zone identifier. + /// IANA Time Zone Database identifier used to interpret the expression, for + /// example `"UTC"` or `"Europe/Berlin"`. pub time_zone: String, } +/// Starts runs from a recurring cron schedule evaluated by the host. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationScheduleTrigger { - /// Stable within the automation definition. + /// Identifier unique and stable within this automation definition. Run causes + /// refer back to this value. pub id: String, + /// Recurrence and time zone evaluated by the host. pub schedule: AutomationSchedule, + /// Policy for missed occurrences. Omission is equivalent to + /// {@link AutomationMisfirePolicy.RunOnce}. #[serde(default, skip_serializing_if = "Option::is_none")] pub misfire_policy: Option, } +/// Starts runs from events understood by the owning host. +/// +/// Event trigger types, event ids, and configuration are discovered through +/// `listAutomationTriggerDefinitions`. A client that does not understand a +/// host-defined trigger can still preserve and display it without interpreting +/// its configuration. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationEventTrigger { - /// Stable within the automation definition. + /// Identifier unique and stable within this automation definition. Run causes + /// refer back to this value. pub id: String, - /// Stable host-defined trigger type. + /// Matches {@link AutomationTriggerDefinition.type}. pub r#type: String, - /// Selected event actions. + /// Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + /// trigger type. pub events: Vec, - /// Schema-defined values. Unknown entries must survive round-trips. + /// Values described by {@link AutomationTriggerDefinition.configSchema}. + /// Clients MUST preserve unknown entries when editing other fields. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, } +/// One selectable event exposed by a host-defined trigger type. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationTriggerEventDefinition { + /// Stable event id stored in {@link AutomationEventTrigger.events}. pub id: String, + /// Human-readable label suitable for selection UI. pub title: String, + /// Optional longer explanation of when this event fires. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, } +/// Describes one host-defined event trigger type available for a prospective +/// automation session template. +/// +/// Trigger definitions are discovery metadata, not durable automation state. +/// Hosts may return different definitions for different providers, working +/// directories, or session configuration. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationTriggerDefinition { + /// Stable type id stored in {@link AutomationEventTrigger.type}. pub r#type: String, + /// Human-readable trigger type name. pub title: String, + /// Optional longer explanation of the trigger source. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, + /// Events clients may select for this trigger type. pub events: Vec, + /// Optional schema for {@link AutomationEventTrigger.config}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config_schema: Option, } +/// Template from which the host creates a fresh session for each automation run. +/// +/// The host revalidates every selection when the run starts. Definitions never +/// carry credentials, confirmation decisions, or durable permission grants. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AutomationSessionTemplate { + /// Provider id. Omit to use the host's default provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, + /// Optional model selection resolved when a run starts. #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, + /// Optional custom agent selection resolved when a run starts. #[serde(default, skip_serializing_if = "Option::is_none")] pub agent: Option, - /// Absence means a workspace-less session. + /// Ordered working-directory URIs for each created session. Absence means a + /// workspace-less session. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// Values resolved through `resolveSessionConfig`. + /// Session configuration values accepted by `createSession`, normally + /// obtained from `resolveSessionConfig`. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, } +/// Durable, client-editable definition of an automation. +/// +/// A definition combines the initial user message, the session template used +/// for each run, and zero or more automatic triggers. Runtime state, run +/// history, revisions, timestamps, and currently allowed operations live on +/// {@link AutomationState} rather than in the definition. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationDefinition { + /// Human-readable automation name. pub title: String, - /// Initial user message sent to each new session. + /// Initial message sent to every newly created run session. Its origin MUST be + /// `user`. pub message: Message, + /// Template used to create fresh sessions for each run. pub session: AutomationSessionTemplate, - /// Controls automatic triggers; manual runs remain permitted. + /// Whether automatic triggers may create runs. Manual runs remain available + /// whenever {@link AutomationOperation.Run} is advertised. pub enabled: bool, - /// Empty means manual-only. + /// Automatic triggers. An empty list means manual-only. pub triggers: Vec, + /// Opaque implementation-defined metadata. Clients MUST preserve unknown + /// entries when updating the definition. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } +/// Host-resolved execution context that is useful to clients but is not part of +/// the editable definition. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AutomationRuntimeState { + /// Effective working directories after host-side preparation, such as + /// materializing a managed workspace. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// Opaque host-defined runtime metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } +/// Lightweight root-catalogue projection of an automation. +/// +/// Returned by `listAutomations` and carried by root automation notifications, +/// this contains enough information to render a list without subscribing to +/// every `ahp-automation:` resource. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationSummary { + /// Subscribable `ahp-automation:` URI. pub resource: Uri, + /// Current {@link AutomationDefinition.title}. pub title: String, + /// Current {@link AutomationDefinition.enabled} value. pub enabled: bool, + /// Number of automatic triggers in the current definition. pub trigger_count: i64, + /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. #[serde(default, skip_serializing_if = "Option::is_none")] pub next_run_at: Option, + /// Most recent retained run, when any run exists. #[serde(default, skip_serializing_if = "Option::is_none")] pub last_run: Option, + /// Monotonic definition revision used for optimistic concurrency. pub revision: i64, + /// Operations currently permitted for this automation. pub operations: Vec, + /// Creation timestamp in ISO 8601 format. pub created_at: String, + /// Last definition modification timestamp in ISO 8601 format. pub modified_at: String, + /// Opaque host-defined catalogue metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } +/// Authoritative state of one subscribed `ahp-automation:` resource. +/// +/// The host owns definition revisions, trigger evaluation, run claims, run +/// retention, and operation availability. Clients render this state and submit +/// commands; they never run a fallback scheduler for a host-owned definition. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationState { + /// URI of this automation channel. pub resource: Uri, + /// Current durable definition. pub definition: AutomationDefinition, + /// Monotonically increasing definition revision. Clients pass the revision + /// they observed as `updateAutomation.expectedRevision`. pub revision: i64, + /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. #[serde(default, skip_serializing_if = "Option::is_none")] pub next_run_at: Option, - /// Newest-first retained run summaries. + /// Newest-first retained run summaries. This is a bounded window; use + /// `fetchAutomationRuns` when {@link runsNextCursor} is present. pub runs: Vec, + /// Opaque cursor for the next older run-history page. #[serde(default, skip_serializing_if = "Option::is_none")] pub runs_next_cursor: Option, + /// Optional host-resolved execution context. #[serde(default, skip_serializing_if = "Option::is_none")] pub runtime: Option, + /// Operations currently permitted for this automation. pub operations: Vec, + /// Creation timestamp in ISO 8601 format. pub created_at: String, + /// Last definition modification timestamp in ISO 8601 format. pub modified_at: String, + /// Opaque host-defined state metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } +/// Summary of why a run cannot currently make progress. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunBlocker { + /// Category of the outstanding dependency. pub kind: AutomationRunBlockerKind, } +/// Cause recorded for a client-requested manual run. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationManualRunCause {} +/// Cause recorded for a run created by one of the automation's triggers. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationTriggeredRunCause { + /// Matches the stable {@link AutomationTrigger.id} in the definition. pub trigger_id: String, + /// Intended schedule occurrence as an ISO 8601 timestamp. Present for + /// schedule triggers and normally absent for event triggers. #[serde(default, skip_serializing_if = "Option::is_none")] pub scheduled_for: Option, + /// `true` when this is a catch-up run created by + /// {@link AutomationMisfirePolicy.RunOnce}. #[serde(default, skip_serializing_if = "Option::is_none")] pub catch_up: Option, - /// Host-defined event provenance containing no secrets. + /// Host-defined, non-secret event provenance suitable for display or audit. + /// This is descriptive context, not an input that clients replay. #[serde(default, skip_serializing_if = "Option::is_none")] pub event: Option, } +/// A durable run exists but has not begun external execution. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationPendingRunLifecycle { + /// Run creation timestamp in ISO 8601 format. pub created_at: String, } +/// The run is actively executing linked sessions. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunningRunLifecycle { + /// Run creation timestamp in ISO 8601 format. pub created_at: String, + /// First execution start timestamp in ISO 8601 format. pub started_at: String, } +/// The run started but is temporarily unable to progress. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationBlockedRunLifecycle { + /// Run creation timestamp in ISO 8601 format. pub created_at: String, + /// First execution start timestamp in ISO 8601 format. pub started_at: String, + /// Coarse blocker summary; linked sessions contain interaction details. pub blocker: AutomationRunBlocker, } +/// Terminal lifecycle for a successfully completed run. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationCompletedRunLifecycle { + /// Run creation timestamp in ISO 8601 format. pub created_at: String, + /// First execution start timestamp in ISO 8601 format. pub started_at: String, + /// Completion timestamp in ISO 8601 format. pub completed_at: String, + /// Optional aggregate model usage across all linked sessions. #[serde(default, skip_serializing_if = "Option::is_none")] pub usage: Option, } +/// Terminal lifecycle for a run that ended with an error. +/// +/// `startedAt` is absent when failure occurred before execution began, such as +/// session-template validation or workspace preparation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationFailedRunLifecycle { + /// Run creation timestamp in ISO 8601 format. pub created_at: String, + /// First execution start timestamp in ISO 8601 format, when execution began. #[serde(default, skip_serializing_if = "Option::is_none")] pub started_at: Option, + /// Failure timestamp in ISO 8601 format. pub completed_at: String, + /// Stable machine-readable and human-readable failure information. pub error: ErrorInfo, } +/// Terminal lifecycle for a cancelled run. +/// +/// `startedAt` is absent when cancellation completed while the run was still +/// pending. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationCancelledRunLifecycle { + /// Run creation timestamp in ISO 8601 format. pub created_at: String, + /// First execution start timestamp in ISO 8601 format, when execution began. #[serde(default, skip_serializing_if = "Option::is_none")] pub started_at: Option, + /// Cancellation completion timestamp in ISO 8601 format. pub completed_at: String, } +/// Fetchable output produced at run scope rather than by one specific session. +/// +/// The inherited {@link ContentRef} identifies how the client obtains the +/// content. Session-specific edits, transcripts, and tool results remain on +/// their session and chat channels. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunArtifact { @@ -4633,41 +4792,73 @@ pub struct AutomationRunArtifact { /// Content nonce #[serde(default, skip_serializing_if = "Option::is_none")] pub nonce: Option, + /// Stable artifact id within this run, used by artifact actions. pub id: String, + /// Human-readable label suitable for run-history UI. pub label: String, + /// Opaque host-defined artifact metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } +/// Lightweight projection of a run retained in its automation's history. +/// +/// A summary contains enough information to render run history without +/// subscribing to every `ahp-automation-run:` resource. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunSummary { + /// Subscribable `ahp-automation-run:` URI. pub resource: Uri, + /// Owning `ahp-automation:` URI. pub automation: Uri, + /// Immutable reason this run was created. pub cause: AutomationRunCause, + /// Current or terminal lifecycle snapshot. pub lifecycle: AutomationRunLifecycle, + /// Session the host recommends opening first, when one has been selected. #[serde(default, skip_serializing_if = "Option::is_none")] pub primary_session: Option, + /// Number of linked sessions, including attempts and workers. pub session_count: i64, + /// Number of run-scoped artifacts, when cheaply available. #[serde(default, skip_serializing_if = "Option::is_none")] pub artifact_count: Option, + /// Operations currently permitted for this run. pub operations: Vec, + /// Opaque host-defined summary metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } +/// Authoritative state of one subscribed `ahp-automation-run:` resource. +/// +/// The run channel owns task-level lifecycle, provenance, linked-session +/// membership, artifacts, and cancellation availability. Linked session and +/// chat channels remain authoritative for transcripts, tools, confirmations, +/// changesets, and per-session lifecycle. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AutomationRunState { + /// URI of this automation-run channel. pub resource: Uri, + /// Owning `ahp-automation:` URI. pub automation: Uri, + /// Immutable reason this run was created. pub cause: AutomationRunCause, + /// Current or terminal lifecycle. pub lifecycle: AutomationRunLifecycle, + /// Ordered, unique session URIs belonging to this run. Entries may represent + /// retries, parallel workers, or delegated attempts. pub sessions: Vec, + /// Session the host recommends opening first, when one has been selected. #[serde(default, skip_serializing_if = "Option::is_none")] pub primary_session: Option, + /// Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}. pub artifacts: Vec, + /// Operations currently permitted for this run. pub operations: Vec, + /// Opaque host-defined run metadata. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, } @@ -5043,20 +5234,6 @@ pub enum SessionOrigin { Automation(AutomationSessionOrigin), } -/// Calendar schedule for an automation trigger. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "kind")] -pub enum AutomationSchedule { - #[serde(rename = "hourly")] - Hourly(AutomationHourlySchedule), - #[serde(rename = "daily")] - Daily(AutomationDailySchedule), - #[serde(rename = "weekly")] - Weekly(AutomationWeeklySchedule), - #[serde(rename = "cron")] - Cron(AutomationCronSchedule), -} - /// Automatic trigger for an automation. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 1524d60a5..9ff2d66a6 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1980,9 +1980,13 @@ public struct ResourceWatchChangedAction: Codable, Sendable { public struct AutomationDefinitionChangedAction: Codable, Sendable { public var type: ActionType + /// Complete replacement definition. public var definition: AutomationDefinition + /// New monotonic revision. public var revision: Int + /// Definition modification timestamp in ISO 8601 format. public var modifiedAt: String + /// Earliest known future scheduled occurrence, or omitted to clear it. public var nextRunAt: String? public init( @@ -2002,6 +2006,7 @@ public struct AutomationDefinitionChangedAction: Codable, Sendable { public struct AutomationRunSummarySetAction: Codable, Sendable { public var type: ActionType + /// New or replacement run summary. public var run: AutomationRunSummary public init( @@ -2015,6 +2020,7 @@ public struct AutomationRunSummarySetAction: Codable, Sendable { public struct AutomationRunSummaryRemovedAction: Codable, Sendable { public var type: ActionType + /// {@link AutomationRunSummary.resource} to remove. public var run: String public init( @@ -2028,7 +2034,9 @@ public struct AutomationRunSummaryRemovedAction: Codable, Sendable { public struct AutomationRunsLoadedAction: Codable, Sendable { public var type: ActionType + /// Older run summaries in newest-first order within this page. public var runs: [AutomationRunSummary] + /// Opaque cursor for the next older page, or omitted at the end. public var nextCursor: String? public init( @@ -2044,7 +2052,9 @@ public struct AutomationRunsLoadedAction: Codable, Sendable { public struct AutomationRunLifecycleChangedAction: Codable, Sendable { public var type: ActionType + /// Complete replacement lifecycle. public var lifecycle: AutomationRunLifecycle + /// Complete replacement operation list. public var operations: [AutomationRunOperation] public init( @@ -2060,6 +2070,7 @@ public struct AutomationRunLifecycleChangedAction: Codable, Sendable { public struct AutomationRunSessionSetAction: Codable, Sendable { public var type: ActionType + /// Session URI to append when it is not already linked. public var session: String public init( @@ -2073,6 +2084,7 @@ public struct AutomationRunSessionSetAction: Codable, Sendable { public struct AutomationRunSessionRemovedAction: Codable, Sendable { public var type: ActionType + /// Linked session URI to remove. public var session: String public init( @@ -2086,6 +2098,7 @@ public struct AutomationRunSessionRemovedAction: Codable, Sendable { public struct AutomationRunPrimarySessionChangedAction: Codable, Sendable { public var type: ActionType + /// New primary linked session, or omitted to clear the selection. public var primarySession: String? public init( @@ -2099,6 +2112,7 @@ public struct AutomationRunPrimarySessionChangedAction: Codable, Sendable { public struct AutomationRunArtifactSetAction: Codable, Sendable { public var type: ActionType + /// New or replacement artifact. public var artifact: AutomationRunArtifact public init( @@ -2112,6 +2126,7 @@ public struct AutomationRunArtifactSetAction: Codable, Sendable { public struct AutomationRunArtifactRemovedAction: Codable, Sendable { public var type: ActionType + /// {@link AutomationRunArtifact.id} to remove. public var artifactId: String public init( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index b84c8a8e0..78f26f2b1 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -266,7 +266,8 @@ public struct InitializeResult: Codable, Sendable { /// defines a template variable, `{level}`, for subscriber-side severity /// filtering). Clients MAY ignore signals they cannot process. public var telemetry: TelemetryCapabilities? - /// Host automation support. Absence means unsupported. + /// Host-owned automation support. Absence means the host does not expose an + /// automation catalogue or automation commands. public var automations: AutomationCapabilities? public init( @@ -314,11 +315,19 @@ public struct ClientCapabilities: Codable, Sendable { } public struct AutomationCapabilities: Codable, Sendable { + /// Availability guarantee for automatic trigger execution. public var execution: AutomationExecutionCapabilities + /// Present when clients may call `createAutomation`. public var create: AutomationCreateCapability? + /// Present when definitions may contain schedule triggers. public var schedules: AutomationScheduleCapabilities? + /// Present when clients may request cancellation on eligible runs. public var runCancellation: AutomationRunCancellationCapability? + /// Present when clients may call `previewAutomationSchedule`. public var schedulePreview: AutomationSchedulePreviewCapability? + /// Maximum terminal run summaries retained per automation. Active runs are not + /// counted toward the limit. Absence means the retention limit is + /// implementation-defined. public var runHistoryLimit: Int? public init( @@ -339,6 +348,7 @@ public struct AutomationCapabilities: Codable, Sendable { } public struct AutomationExecutionCapabilities: Codable, Sendable { + /// How long automatic trigger evaluation remains available. public var lifetime: AutomationExecutionLifetime public init( @@ -357,27 +367,13 @@ public struct AutomationCreateCapability: Codable, Sendable { } public struct AutomationScheduleCapabilities: Codable, Sendable { - public var kinds: [AutomationScheduleKind] - public var cron: AutomationCronScheduleCapability? - - public init( - kinds: [AutomationScheduleKind], - cron: AutomationCronScheduleCapability? = nil - ) { - self.kinds = kinds - self.cron = cron - } -} - -public struct AutomationCronScheduleCapability: Codable, Sendable { - public var dialect: String + /// Smallest permitted interval between consecutive occurrences. Omission + /// means no restriction beyond the cron format's one-minute resolution. public var minIntervalMinutes: Int? public init( - dialect: String, minIntervalMinutes: Int? = nil ) { - self.dialect = dialect self.minIntervalMinutes = minIntervalMinutes } } @@ -1924,6 +1920,7 @@ public struct ListAutomationsParams: Codable, Sendable { /// as opaque — do not parse, modify, or persist them across connections. An /// unrecognised cursor SHOULD be rejected with an `InvalidParams` error. public var cursor: String? + /// Optional exact filter on {@link AutomationDefinition.enabled}. public var enabled: Bool? enum CodingKeys: String, CodingKey { @@ -1954,6 +1951,7 @@ public struct ListAutomationsResult: Codable, Sendable { /// returned page; absent signals the end of the collection. Pass it back as /// {@link PaginatedParams.cursor} to fetch the following page. public var nextCursor: String? + /// Automation summaries in host-defined catalogue order. public var items: [AutomationSummary] public init( @@ -1971,8 +1969,11 @@ public struct ListAutomationTriggerDefinitionsParams: Codable, Sendable { /// Optional JSON-serializable metadata associated with this request. /// Receivers MUST ignore keys they do not understand. public var meta: [String: AnyCodable]? + /// Prospective provider id, or omitted for the host default. public var provider: String? + /// Prospective ordered working-directory list. public var workingDirectories: [String]? + /// Prospective resolved session configuration values. public var sessionConfig: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -1999,6 +2000,7 @@ public struct ListAutomationTriggerDefinitionsParams: Codable, Sendable { } public struct ListAutomationTriggerDefinitionsResult: Codable, Sendable { + /// Available event trigger definitions. public var items: [AutomationTriggerDefinition] public init( @@ -2014,8 +2016,10 @@ public struct CreateAutomationParams: Codable, Sendable { /// Optional JSON-serializable metadata associated with this request. /// Receivers MUST ignore keys they do not understand. public var meta: [String: AnyCodable]? + /// Complete initial definition. public var definition: AutomationDefinition - public var `import`: AnyCodable? + /// Optional idempotency identity when importing a legacy definition. + public var `import`: AutomationImportIdentity? enum CodingKeys: String, CodingKey { case channel @@ -2028,7 +2032,7 @@ public struct CreateAutomationParams: Codable, Sendable { channel: String, meta: [String: AnyCodable]? = nil, definition: AutomationDefinition, - `import`: AnyCodable? = nil + `import`: AutomationImportIdentity? = nil ) { self.channel = channel self.meta = meta @@ -2037,12 +2041,37 @@ public struct CreateAutomationParams: Codable, Sendable { } } +public struct AutomationImportIdentity: Codable, Sendable { + /// Stable namespace identifying the source implementation or store. + public var source: String + /// Identifier shared by every item in one import attempt. + public var batchId: String + /// Stable source-side identifier for this definition within the batch. + public var itemId: String + + public init( + source: String, + batchId: String, + itemId: String + ) { + self.source = source + self.batchId = batchId + self.itemId = itemId + } +} + public struct AutomationDefinitionPatch: Codable, Sendable { + /// Replacement human-readable title. public var title: String? + /// Replacement initial user message. public var message: Message? + /// Replacement session template. public var session: AutomationSessionTemplate? + /// Replacement automatic-trigger enabled state. public var enabled: Bool? + /// Complete replacement trigger list. public var triggers: [AutomationTrigger]? + /// Complete replacement implementation-defined metadata. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -2077,7 +2106,9 @@ public struct UpdateAutomationParams: Codable, Sendable { /// Optional JSON-serializable metadata associated with this request. /// Receivers MUST ignore keys they do not understand. public var meta: [String: AnyCodable]? + /// Revision on which the client based {@link changes}. public var expectedRevision: Int + /// Editable fields to replace. public var changes: AutomationDefinitionPatch enum CodingKeys: String, CodingKey { @@ -2127,6 +2158,9 @@ public struct RunAutomationParams: Codable, Sendable { /// Optional JSON-serializable metadata associated with this request. /// Receivers MUST ignore keys they do not understand. public var meta: [String: AnyCodable]? + /// Durable client-generated idempotency key. Retrying with the same key and + /// automation MUST return the original run URI rather than create another + /// run. public var requestId: String enum CodingKeys: String, CodingKey { @@ -2147,6 +2181,7 @@ public struct RunAutomationParams: Codable, Sendable { } public struct RunAutomationResult: Codable, Sendable { + /// Subscribable `ahp-automation-run:` URI. public var run: String public init( @@ -2162,6 +2197,8 @@ public struct FetchAutomationRunsParams: Codable, Sendable { /// Optional JSON-serializable metadata associated with this request. /// Receivers MUST ignore keys they do not understand. public var meta: [String: AnyCodable]? + /// Cursor previously received as {@link AutomationState.runsNextCursor}. + /// Omit to request the first page not already included by the snapshot. public var cursor: String? enum CodingKeys: String, CodingKey { @@ -2195,7 +2232,9 @@ public struct PreviewAutomationScheduleParams: Codable, Sendable { /// Optional JSON-serializable metadata associated with this request. /// Receivers MUST ignore keys they do not understand. public var meta: [String: AnyCodable]? + /// Portable AHP cron schedule to evaluate. public var schedule: AutomationSchedule + /// Requested maximum number of future occurrences; the host MAY cap it. public var count: Int? enum CodingKeys: String, CodingKey { @@ -2219,6 +2258,7 @@ public struct PreviewAutomationScheduleParams: Codable, Sendable { } public struct PreviewAutomationScheduleResult: Codable, Sendable { + /// Ascending ISO 8601 timestamps. public var items: [String] public init( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index eadc50f8d..ef57a0b9d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -67,7 +67,9 @@ public struct SessionSummaryChangedParams: Codable, Sendable { } public struct AutomationAddedParams: Codable, Sendable { + /// Root channel URI. public var channel: String + /// Complete summary for the newly visible automation. public var summary: AutomationSummary public init( @@ -80,7 +82,9 @@ public struct AutomationAddedParams: Codable, Sendable { } public struct AutomationRemovedParams: Codable, Sendable { + /// Root channel URI. public var channel: String + /// Removed `ahp-automation:` URI. public var automation: String public init( @@ -93,7 +97,9 @@ public struct AutomationRemovedParams: Codable, Sendable { } public struct AutomationSummaryChangedParams: Codable, Sendable { + /// Root channel URI. public var channel: String + /// Complete replacement catalogue summary. public var summary: AutomationSummary public init( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 5ad3d9415..37f69cb1d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -400,70 +400,107 @@ public enum ResourceChangeType: String, Codable, Sendable { case deleted = "deleted" } +/// Discriminant describing the durable provenance of a session. public enum SessionOriginKind: String, Codable, Sendable { + /// The session was created as part of an automation run. case automation = "automation" } +/// Operations the host currently permits for an automation. +/// +/// The list on {@link AutomationState.operations} is authoritative and may +/// change over time. Clients MUST NOT infer permission from capabilities alone: +/// capabilities describe what the host implementation can support, while +/// operations describe what is allowed for this particular automation now. public enum AutomationOperation: String, Codable, Sendable { + /// Replace editable fields using `updateAutomation`. case update = "update" + /// Permanently remove the automation using `disposeAutomation`. case dispose = "dispose" + /// Start a manual run using `runAutomation`. case run = "run" } +/// Availability guarantee for host-owned automatic trigger evaluation. +/// +/// This describes the authority that owns one automation catalogue. It does not +/// prevent a client from connecting to several authorities with different +/// lifetimes (for example, one local host and one managed service). public enum AutomationExecutionLifetime: String, Codable, Sendable { + /// Automatic triggers are evaluated only while this host process is running. + /// Definitions may remain durable across restarts, but occurrences while the + /// process is unavailable are handled according to the trigger's + /// {@link AutomationMisfirePolicy}. case hostLifetime = "hostLifetime" + /// Automatic triggers continue to be evaluated independently of connected + /// clients and any particular interactive host process. case managed = "managed" } -public enum AutomationScheduleKind: String, Codable, Sendable { - case hourly = "hourly" - case daily = "daily" - case weekly = "weekly" - case cron = "cron" -} - -public enum AutomationWeekday: String, Codable, Sendable { - case monday = "monday" - case tuesday = "tuesday" - case wednesday = "wednesday" - case thursday = "thursday" - case friday = "friday" - case saturday = "saturday" - case sunday = "sunday" -} - +/// How a host handles schedule occurrences missed while automatic execution was +/// unavailable. public enum AutomationMisfirePolicy: String, Codable, Sendable { + /// Discard missed occurrences and wait for the next future occurrence. case skip = "skip" + /// Start at most one catch-up run when execution becomes available, regardless + /// of how many occurrences were missed. case runOnce = "runOnce" } +/// Discriminant for automatic trigger definitions. public enum AutomationTriggerKind: String, Codable, Sendable { + /// A portable recurring {@link AutomationSchedule}. case schedule = "schedule" + /// A host-defined external event discovered from trigger definitions. case event = "event" } +/// Lifecycle status of one automation run. +/// +/// `completed`, `failed`, and `cancelled` are terminal. `blocked` is +/// non-terminal: the host may return the run to `running` after the linked +/// session resolves the blocker. public enum AutomationRunStatus: String, Codable, Sendable { + /// The durable run record exists but execution has not started. case pending = "pending" + /// One or more linked sessions are actively executing. case running = "running" + /// Execution is paused on an interaction or client-side dependency. case blocked = "blocked" + /// Execution finished successfully. case completed = "completed" + /// Execution ended with an error. case failed = "failed" + /// Execution ended because cancellation was accepted. case cancelled = "cancelled" } +/// Coarse reason a run is blocked. +/// +/// Detailed prompts, confirmations, authentication requests, and tool state +/// remain authoritative on linked session and chat channels. public enum AutomationRunBlockerKind: String, Codable, Sendable { + /// A linked session is waiting for an answer to a user-input request. case userInput = "userInput" + /// A linked session is waiting for tool confirmation. case toolConfirmation = "toolConfirmation" + /// Execution requires authentication or renewed credentials. case authentication = "authentication" + /// Work must be performed by or delegated to a connected client. case clientExecution = "clientExecution" } +/// Discriminant describing what created an automation run. public enum AutomationRunCauseKind: String, Codable, Sendable { + /// A client explicitly invoked `runAutomation`. case manual = "manual" + /// An automatic schedule or event trigger fired. case trigger = "trigger" } +/// Operations the host currently permits for a run. public enum AutomationRunOperation: String, Codable, Sendable { + /// Request cancellation with `automationRun/cancelRequested`. case cancel = "cancel" } @@ -5306,7 +5343,9 @@ public struct ResourceChange: Codable, Sendable { public struct AutomationSessionOrigin: Codable, Sendable { public var kind: SessionOriginKind + /// Owning `ahp-automation:` URI. public var automation: String + /// Owning `ahp-automation-run:` URI. public var run: String public init( @@ -5320,89 +5359,31 @@ public struct AutomationSessionOrigin: Codable, Sendable { } } -public struct AutomationLocalTime: Codable, Sendable { - public var hour: Int - public var minute: Int - - public init( - hour: Int, - minute: Int - ) { - self.hour = hour - self.minute = minute - } -} - -public struct AutomationHourlySchedule: Codable, Sendable { - public var kind: AutomationScheduleKind - - public init( - kind: AutomationScheduleKind - ) { - self.kind = kind - } -} - -public struct AutomationDailySchedule: Codable, Sendable { - public var kind: AutomationScheduleKind - public var time: AutomationLocalTime - /// IANA time-zone identifier. - public var timeZone: String - - public init( - kind: AutomationScheduleKind, - time: AutomationLocalTime, - timeZone: String - ) { - self.kind = kind - self.time = time - self.timeZone = timeZone - } -} - -public struct AutomationWeeklySchedule: Codable, Sendable { - public var kind: AutomationScheduleKind - public var weekday: AutomationWeekday - public var time: AutomationLocalTime - /// IANA time-zone identifier. - public var timeZone: String - - public init( - kind: AutomationScheduleKind, - weekday: AutomationWeekday, - time: AutomationLocalTime, - timeZone: String - ) { - self.kind = kind - self.weekday = weekday - self.time = time - self.timeZone = timeZone - } -} - -public struct AutomationCronSchedule: Codable, Sendable { - public var kind: AutomationScheduleKind - /// Standard five-field Unix cron expression. +public struct AutomationSchedule: Codable, Sendable { + /// Five-field AHP cron expression described by {@link AutomationSchedule}. public var expression: String - /// IANA time-zone identifier. + /// IANA Time Zone Database identifier used to interpret the expression, for + /// example `"UTC"` or `"Europe/Berlin"`. public var timeZone: String public init( - kind: AutomationScheduleKind, expression: String, timeZone: String ) { - self.kind = kind self.expression = expression self.timeZone = timeZone } } public struct AutomationScheduleTrigger: Codable, Sendable { - /// Stable within the automation definition. + /// Identifier unique and stable within this automation definition. Run causes + /// refer back to this value. public var id: String public var kind: AutomationTriggerKind + /// Recurrence and time zone evaluated by the host. public var schedule: AutomationSchedule + /// Policy for missed occurrences. Omission is equivalent to + /// {@link AutomationMisfirePolicy.RunOnce}. public var misfirePolicy: AutomationMisfirePolicy? public init( @@ -5419,14 +5400,17 @@ public struct AutomationScheduleTrigger: Codable, Sendable { } public struct AutomationEventTrigger: Codable, Sendable { - /// Stable within the automation definition. + /// Identifier unique and stable within this automation definition. Run causes + /// refer back to this value. public var id: String public var kind: AutomationTriggerKind - /// Stable host-defined trigger type. + /// Matches {@link AutomationTriggerDefinition.type}. public var type: String - /// Selected event actions. + /// Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + /// trigger type. public var events: [String] - /// Schema-defined values. Unknown entries must survive round-trips. + /// Values described by {@link AutomationTriggerDefinition.configSchema}. + /// Clients MUST preserve unknown entries when editing other fields. public var config: [String: AnyCodable]? public init( @@ -5445,8 +5429,11 @@ public struct AutomationEventTrigger: Codable, Sendable { } public struct AutomationTriggerEventDefinition: Codable, Sendable { + /// Stable event id stored in {@link AutomationEventTrigger.events}. public var id: String + /// Human-readable label suitable for selection UI. public var title: String + /// Optional longer explanation of when this event fires. public var description: String? public init( @@ -5461,10 +5448,15 @@ public struct AutomationTriggerEventDefinition: Codable, Sendable { } public struct AutomationTriggerDefinition: Codable, Sendable { + /// Stable type id stored in {@link AutomationEventTrigger.type}. public var type: String + /// Human-readable trigger type name. public var title: String + /// Optional longer explanation of the trigger source. public var description: String? + /// Events clients may select for this trigger type. public var events: [AutomationTriggerEventDefinition] + /// Optional schema for {@link AutomationEventTrigger.config}. public var configSchema: ConfigSchema? public init( @@ -5483,12 +5475,17 @@ public struct AutomationTriggerDefinition: Codable, Sendable { } public struct AutomationSessionTemplate: Codable, Sendable { + /// Provider id. Omit to use the host's default provider. public var provider: String? + /// Optional model selection resolved when a run starts. public var model: ModelSelection? + /// Optional custom agent selection resolved when a run starts. public var agent: AgentSelection? - /// Absence means a workspace-less session. + /// Ordered working-directory URIs for each created session. Absence means a + /// workspace-less session. public var workingDirectories: [String]? - /// Values resolved through `resolveSessionConfig`. + /// Session configuration values accepted by `createSession`, normally + /// obtained from `resolveSessionConfig`. public var config: [String: AnyCodable]? public init( @@ -5507,14 +5504,20 @@ public struct AutomationSessionTemplate: Codable, Sendable { } public struct AutomationDefinition: Codable, Sendable { + /// Human-readable automation name. public var title: String - /// Initial user message sent to each new session. + /// Initial message sent to every newly created run session. Its origin MUST be + /// `user`. public var message: Message + /// Template used to create fresh sessions for each run. public var session: AutomationSessionTemplate - /// Controls automatic triggers; manual runs remain permitted. + /// Whether automatic triggers may create runs. Manual runs remain available + /// whenever {@link AutomationOperation.Run} is advertised. public var enabled: Bool - /// Empty means manual-only. + /// Automatic triggers. An empty list means manual-only. public var triggers: [AutomationTrigger] + /// Opaque implementation-defined metadata. Clients MUST preserve unknown + /// entries when updating the definition. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -5544,7 +5547,10 @@ public struct AutomationDefinition: Codable, Sendable { } public struct AutomationRuntimeState: Codable, Sendable { + /// Effective working directories after host-side preparation, such as + /// materializing a managed workspace. public var workingDirectories: [String]? + /// Opaque host-defined runtime metadata. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -5562,16 +5568,27 @@ public struct AutomationRuntimeState: Codable, Sendable { } public struct AutomationSummary: Codable, Sendable { + /// Subscribable `ahp-automation:` URI. public var resource: String + /// Current {@link AutomationDefinition.title}. public var title: String + /// Current {@link AutomationDefinition.enabled} value. public var enabled: Bool + /// Number of automatic triggers in the current definition. public var triggerCount: Int + /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. public var nextRunAt: String? + /// Most recent retained run, when any run exists. public var lastRun: AutomationRunSummary? + /// Monotonic definition revision used for optimistic concurrency. public var revision: Int + /// Operations currently permitted for this automation. public var operations: [AutomationOperation] + /// Creation timestamp in ISO 8601 format. public var createdAt: String + /// Last definition modification timestamp in ISO 8601 format. public var modifiedAt: String + /// Opaque host-defined catalogue metadata. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -5616,17 +5633,29 @@ public struct AutomationSummary: Codable, Sendable { } public struct AutomationState: Codable, Sendable { + /// URI of this automation channel. public var resource: String + /// Current durable definition. public var definition: AutomationDefinition + /// Monotonically increasing definition revision. Clients pass the revision + /// they observed as `updateAutomation.expectedRevision`. public var revision: Int + /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. public var nextRunAt: String? - /// Newest-first retained run summaries. + /// Newest-first retained run summaries. This is a bounded window; use + /// `fetchAutomationRuns` when {@link runsNextCursor} is present. public var runs: [AutomationRunSummary] + /// Opaque cursor for the next older run-history page. public var runsNextCursor: String? + /// Optional host-resolved execution context. public var runtime: AutomationRuntimeState? + /// Operations currently permitted for this automation. public var operations: [AutomationOperation] + /// Creation timestamp in ISO 8601 format. public var createdAt: String + /// Last definition modification timestamp in ISO 8601 format. public var modifiedAt: String + /// Opaque host-defined state metadata. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -5671,6 +5700,7 @@ public struct AutomationState: Codable, Sendable { } public struct AutomationRunBlocker: Codable, Sendable { + /// Category of the outstanding dependency. public var kind: AutomationRunBlockerKind public init( @@ -5692,10 +5722,16 @@ public struct AutomationManualRunCause: Codable, Sendable { public struct AutomationTriggeredRunCause: Codable, Sendable { public var kind: AutomationRunCauseKind + /// Matches the stable {@link AutomationTrigger.id} in the definition. public var triggerId: String + /// Intended schedule occurrence as an ISO 8601 timestamp. Present for + /// schedule triggers and normally absent for event triggers. public var scheduledFor: String? + /// `true` when this is a catch-up run created by + /// {@link AutomationMisfirePolicy.RunOnce}. public var catchUp: Bool? - /// Host-defined event provenance containing no secrets. + /// Host-defined, non-secret event provenance suitable for display or audit. + /// This is descriptive context, not an input that clients replay. public var event: [String: AnyCodable]? public init( @@ -5715,6 +5751,7 @@ public struct AutomationTriggeredRunCause: Codable, Sendable { public struct AutomationPendingRunLifecycle: Codable, Sendable { public var status: AutomationRunStatus + /// Run creation timestamp in ISO 8601 format. public var createdAt: String public init( @@ -5728,7 +5765,9 @@ public struct AutomationPendingRunLifecycle: Codable, Sendable { public struct AutomationRunningRunLifecycle: Codable, Sendable { public var status: AutomationRunStatus + /// Run creation timestamp in ISO 8601 format. public var createdAt: String + /// First execution start timestamp in ISO 8601 format. public var startedAt: String public init( @@ -5744,8 +5783,11 @@ public struct AutomationRunningRunLifecycle: Codable, Sendable { public struct AutomationBlockedRunLifecycle: Codable, Sendable { public var status: AutomationRunStatus + /// Run creation timestamp in ISO 8601 format. public var createdAt: String + /// First execution start timestamp in ISO 8601 format. public var startedAt: String + /// Coarse blocker summary; linked sessions contain interaction details. public var blocker: AutomationRunBlocker public init( @@ -5763,9 +5805,13 @@ public struct AutomationBlockedRunLifecycle: Codable, Sendable { public struct AutomationCompletedRunLifecycle: Codable, Sendable { public var status: AutomationRunStatus + /// Run creation timestamp in ISO 8601 format. public var createdAt: String + /// First execution start timestamp in ISO 8601 format. public var startedAt: String + /// Completion timestamp in ISO 8601 format. public var completedAt: String + /// Optional aggregate model usage across all linked sessions. public var usage: UsageInfo? public init( @@ -5785,9 +5831,13 @@ public struct AutomationCompletedRunLifecycle: Codable, Sendable { public struct AutomationFailedRunLifecycle: Codable, Sendable { public var status: AutomationRunStatus + /// Run creation timestamp in ISO 8601 format. public var createdAt: String + /// First execution start timestamp in ISO 8601 format, when execution began. public var startedAt: String? + /// Failure timestamp in ISO 8601 format. public var completedAt: String + /// Stable machine-readable and human-readable failure information. public var error: ErrorInfo public init( @@ -5807,8 +5857,11 @@ public struct AutomationFailedRunLifecycle: Codable, Sendable { public struct AutomationCancelledRunLifecycle: Codable, Sendable { public var status: AutomationRunStatus + /// Run creation timestamp in ISO 8601 format. public var createdAt: String + /// First execution start timestamp in ISO 8601 format, when execution began. public var startedAt: String? + /// Cancellation completion timestamp in ISO 8601 format. public var completedAt: String public init( @@ -5833,8 +5886,11 @@ public struct AutomationRunArtifact: Codable, Sendable { public var contentType: String? /// Content nonce public var nonce: String? + /// Stable artifact id within this run, used by artifact actions. public var id: String + /// Human-readable label suitable for run-history UI. public var label: String + /// Opaque host-defined artifact metadata. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -5867,14 +5923,23 @@ public struct AutomationRunArtifact: Codable, Sendable { } public struct AutomationRunSummary: Codable, Sendable { + /// Subscribable `ahp-automation-run:` URI. public var resource: String + /// Owning `ahp-automation:` URI. public var automation: String + /// Immutable reason this run was created. public var cause: AutomationRunCause + /// Current or terminal lifecycle snapshot. public var lifecycle: AutomationRunLifecycle + /// Session the host recommends opening first, when one has been selected. public var primarySession: String? + /// Number of linked sessions, including attempts and workers. public var sessionCount: Int + /// Number of run-scoped artifacts, when cheaply available. public var artifactCount: Int? + /// Operations currently permitted for this run. public var operations: [AutomationRunOperation] + /// Opaque host-defined summary metadata. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -5913,14 +5978,24 @@ public struct AutomationRunSummary: Codable, Sendable { } public struct AutomationRunState: Codable, Sendable { + /// URI of this automation-run channel. public var resource: String + /// Owning `ahp-automation:` URI. public var automation: String + /// Immutable reason this run was created. public var cause: AutomationRunCause + /// Current or terminal lifecycle. public var lifecycle: AutomationRunLifecycle + /// Ordered, unique session URIs belonging to this run. Entries may represent + /// retries, parallel workers, or delegated attempts. public var sessions: [String] + /// Session the host recommends opening first, when one has been selected. public var primarySession: String? + /// Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}. public var artifacts: [AutomationRunArtifact] + /// Operations currently permitted for this run. public var operations: [AutomationRunOperation] + /// Opaque host-defined run metadata. public var meta: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -6737,51 +6812,6 @@ public enum SessionOrigin: Codable, Sendable { } } -public enum AutomationSchedule: Codable, Sendable { - case hourly(AutomationHourlySchedule) - case daily(AutomationDailySchedule) - case weekly(AutomationWeeklySchedule) - case cron(AutomationCronSchedule) - - private enum DiscriminantKey: String, CodingKey { - case discriminant = "kind" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: DiscriminantKey.self) - let discriminant = try container.decode(String.self, forKey: .discriminant) - switch discriminant { - case "hourly": - self = .hourly(try AutomationHourlySchedule(from: decoder)) - case "daily": - self = .daily(try AutomationDailySchedule(from: decoder)) - case "weekly": - self = .weekly(try AutomationWeeklySchedule(from: decoder)) - case "cron": - self = .cron(try AutomationCronSchedule(from: decoder)) - default: - throw DecodingError.dataCorruptedError(forKey: .discriminant, in: container, debugDescription: "Unknown AutomationSchedule discriminant: \(discriminant)") - } - } - - public func encode(to encoder: Encoder) throws { - switch self { - case .hourly(var value): - value.kind = .hourly - try value.encode(to: encoder) - case .daily(var value): - value.kind = .daily - try value.encode(to: encoder) - case .weekly(var value): - value.kind = .weekly - try value.encode(to: encoder) - case .cron(var value): - value.kind = .cron - try value.encode(to: encoder) - } - } -} - public enum AutomationTrigger: Codable, Sendable { case schedule(AutomationScheduleTrigger) case event(AutomationEventTrigger) diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift deleted file mode 100644 index 41b28b255..000000000 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/AutomationUnionTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import XCTest -@testable import AgentHostProtocol - -final class AutomationUnionTests: XCTestCase { - func testScheduleEncodesUnionDiscriminator() throws { - let schedule = AutomationSchedule.daily(AutomationDailySchedule( - kind: .hourly, - time: AutomationLocalTime(hour: 9, minute: 0), - timeZone: "UTC" - )) - let data = try JSONEncoder().encode(schedule) - let object = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) - XCTAssertEqual(object["kind"] as? String, "daily") - } -} diff --git a/docs/guide/automations.md b/docs/guide/automations.md index 878938792..64e239adc 100644 --- a/docs/guide/automations.md +++ b/docs/guide/automations.md @@ -1,46 +1,481 @@ # Automations -Automations let several AHP clients share durable agent-session workflows -without each client running its own scheduler. +Automations are durable, host-owned definitions that create fresh agent +sessions manually or in response to recurring schedules and external events. +They let multiple AHP clients share one catalogue, scheduler, execution claim, +and run history instead of each client maintaining a local copy. -## Mental model +## Key design points -- An **automation** is a saved session template, initial user message, and - trigger collection. -- A **run** is one task-level execution. -- A **session** is one attempt or worker belonging to that run. +- **The host is the single writable authority.** It persists definitions, + evaluates triggers, claims occurrences, creates runs, and records history. +- **Definitions and executions have separate channels.** An + `ahp-automation:` channel owns the reusable definition; each + `ahp-automation-run:` channel owns one task-level execution. +- **Sessions remain ordinary AHP sessions.** A run may link one or more + `ahp-session:` channels, which remain authoritative for transcripts, tool + calls, confirmations, changesets, and per-session lifecycle. +- **Automatic execution never belongs to the client.** A client may render, + edit, run, cancel, and migrate automations, but it does not run a fallback + scheduler after an uncertain host response. +- **Capabilities and operations are distinct.** Initialize capabilities say + what a host implementation supports. State-level `operations` say what is + allowed for one automation or run at this moment. + +## Negotiating support + +A host advertises automation support in `InitializeResult.automations`: + +```typescript +AutomationCapabilities { + execution: { + lifetime: 'hostLifetime' | 'managed' + } + create?: {} + schedules?: { + minIntervalMinutes?: number + } + runCancellation?: {} + schedulePreview?: {} + runHistoryLimit?: number +} +``` + +If `automations` is absent, the client treats the authority as having no +automation catalogue. + +`create`, `runCancellation`, and `schedulePreview` are presence capabilities: +an empty object means the feature is supported, and absence means it is not. +The object shape leaves room for future feature-specific options without +changing capability detection. + +`execution.lifetime` describes automatic-trigger availability: + +| Lifetime | Guarantee | +| --- | --- | +| `hostLifetime` | Triggers are evaluated only while this host process is running. Missed occurrences follow the trigger's misfire policy. | +| `managed` | Triggers continue to be evaluated independently of connected clients and any particular interactive host process. | + +Lifetime applies to one host authority. A client can connect to several +authorities at once—for example, local host-lifetime automations and managed +cloud automations—without combining their catalogues or execution ownership. + +## Resource model + +```mermaid +flowchart TD + R["ahp-root:// catalogue"] --> A["ahp-automation:/<id>"] + A --> RS["newest-first run summaries"] + RS --> AR["ahp-automation-run:/<id>"] + AR --> S1["ahp-session:/<id>"] + AR --> S2["ahp-session:/<id>"] + AR --> F["run-scoped artifacts"] +``` + +The resource layers answer different questions: + +| Resource | Owns | +| --- | --- | +| `ahp-root://` | Lightweight automation catalogue and live catalogue notifications | +| `ahp-automation:` | Definition, revision, next schedule occurrence, retained run summaries, and permitted operations | +| `ahp-automation-run:` | Execution provenance, lifecycle, linked sessions, run-scoped artifacts, and cancellation | +| `ahp-session:` / `ahp-chat:` | Conversation, tools, confirmations, input requests, changesets, and session-specific state | + +## Catalogue and subscriptions + +Clients fetch the catalogue with `listAutomations`. Each +`AutomationSummary` contains enough information to render a list without +subscribing to every automation: + +```typescript +AutomationSummary { + resource: URI + title: string + enabled: boolean + triggerCount: number + nextRunAt?: string + lastRun?: AutomationRunSummary + revision: number + operations: AutomationOperation[] + createdAt: string + modifiedAt: string +} +``` + +Root subscribers receive live catalogue changes: + +| Notification | Meaning | +| --- | --- | +| `root/automationAdded` | A complete summary became visible. | +| `root/automationRemoved` | An automation URI left the catalogue. | +| `root/automationSummaryChanged` | A complete replacement summary is available. | + +Root notifications are not replayed. After reconnect, clients call +`listAutomations` again and reconcile by resource URI. + +Subscribing to an `ahp-automation:` URI returns the full +`AutomationState`, including the definition and retained run-history window. + +## Definitions + +An `AutomationDefinition` is the durable, editable part of an automation: + +```typescript +AutomationDefinition { + title: string + message: Message + session: AutomationSessionTemplate + enabled: boolean + triggers: AutomationTrigger[] + _meta?: Record +} +``` + +`message` is the initial user message sent to every newly created run session. +Its origin must be `user`. The definition does not carry credentials, +confirmation decisions, or durable permission grants. + +### Session template + +The session template selects the context used to create a fresh session for +each run: + +```typescript +AutomationSessionTemplate { + provider?: string + model?: ModelSelection + agent?: AgentSelection + workingDirectories?: URI[] + config?: Record +} +``` + +Omitting `provider` uses the host's default provider. Omitting +`workingDirectories` creates a workspace-less session. The host revalidates +the provider, model, agent, directories, and configuration every time a run +starts because availability and policy may have changed since the definition +was saved. + +Host-prepared execution details—such as materialized managed workspace +directories—belong in `AutomationState.runtime`, not in the editable +definition. + +### Enabled state + +`enabled` controls automatic triggers only. A disabled automation can still be +started manually when its state advertises the `run` operation. This allows a +user to pause scheduling without losing the definition or its ability to run +on demand. + +## Triggers + +An empty `triggers` list means manual-only. Automatic triggers are either +portable schedules or host-defined events. + +Each trigger has an `id` that is unique and stable within its definition. Runs +created automatically record that id in `AutomationTriggeredRunCause`, so +clients can explain why the run exists even if they do not understand the +trigger's configuration. + +### Scheduled triggers + +A scheduled trigger contains a five-field AHP cron expression and an IANA time +zone: + +```typescript +AutomationScheduleTrigger { + id: string + kind: 'schedule' + schedule: { + expression: string + timeZone: string + } + misfirePolicy?: 'skip' | 'runOnce' +} +``` + +The expression has exactly five whitespace-separated fields: ```text -automation - -> run summary - -> automation-run channel - -> one or more session channels +minute hour day-of-month month day-of-week +``` + +| Field | Allowed values | +| --- | --- | +| minute | `0`–`59` | +| hour | `0`–`23` | +| day of month | `1`–`31` | +| month | `1`–`12` or the case-insensitive names `JAN`–`DEC` | +| day of week | `0`–`7` or the case-insensitive names `SUN`–`SAT`; both `0` and `7` mean Sunday | + +Each field supports: + +- `*` for every value; +- one value, such as `9` or `MON`; +- an inclusive range, such as `1-5` or `MON-FRI`; +- a comma-separated list of values or ranges, such as `1,3,8-10`; and +- a positive step on `*` or a range, such as `*/15` or `1-30/2`. + +AHP does not support a seconds field, a year field, macros such as `@daily`, or +Quartz extensions such as `?`, `L`, `W`, and `#`. + +Minute, hour, and month must all match. If both day-of-month and day-of-week +are restricted (not `*`), the occurrence matches when **either** day field +matches, following Unix cron behavior. + +Examples: + +| Desired schedule | Expression | +| --- | --- | +| Every hour | `0 * * * *` | +| Every day at 09:30 | `30 9 * * *` | +| Every weekday at 09:30 | `30 9 * * MON-FRI` | +| Midnight on the 1st and 15th | `0 0 1,15 * *` | +| Every 15 minutes | `*/15 * * * *` | + +The host evaluates the fields against local calendar time in `timeZone`. A +nonexistent local minute during a daylight-saving transition produces no +occurrence; a repeated local minute represents each matching instant. Clients +should use `previewAutomationSchedule`, when advertised, rather than +implementing independent time-zone evaluation. The host's preview and +`nextRunAt` projection are canonical. + +When `AutomationCapabilities.schedules.minIntervalMinutes` is present, the +host rejects schedules that can produce consecutive occurrences more +frequently than that limit. Without it, the cron format's one-minute resolution +is the only interval restriction. + +### Misfires + +A misfire is a scheduled occurrence that happens while automatic execution is +unavailable, for example while a host-lifetime authority is stopped: + +| Policy | Behavior | +| --- | --- | +| `skip` | Discard missed occurrences and wait for the next future occurrence. | +| `runOnce` | Start at most one catch-up run when execution becomes available, regardless of how many occurrences were missed. | + +Omitting `misfirePolicy` is equivalent to `runOnce`. A catch-up run records +`cause.catchUp: true` and the original `scheduledFor` timestamp. + +### Event triggers + +Event triggers are defined by the host rather than standardized by AHP. A +GitHub-aware managed authority might expose events such as pull-request +creation; a local authority may expose no event triggers. + +Clients discover event types with `listAutomationTriggerDefinitions`, passing +the prospective provider, working directories, and resolved session +configuration. The host returns: + +```typescript +AutomationTriggerDefinition { + type: string + title: string + description?: string + events: { + id: string + title: string + description?: string + }[] + configSchema?: ConfigSchema +} +``` + +The durable trigger stores the selected type, event ids, and schema-defined +configuration: + +```typescript +AutomationEventTrigger { + id: string + kind: 'event' + type: string + events: string[] + config?: Record +} +``` + +Unknown configuration entries must survive client edits. Event provenance +recorded on a run must contain no secrets; it is descriptive context, not a +payload clients should replay. + +## Creating and updating + +| Command | Purpose | +| --- | --- | +| `createAutomation` | Persist a complete definition at a client-chosen `ahp-automation:` URI. | +| `updateAutomation` | Replace selected editable fields using an expected revision. | +| `disposeAutomation` | Permanently remove a definition when disposal is currently allowed. | + +Definition revisions increase monotonically. `updateAutomation` includes the +revision the client observed: + +```typescript +{ + channel: 'ahp-automation:/triage' + expectedRevision: 7 + changes: { + enabled: false + } +} +``` + +The host rejects a stale revision. The client then reconciles the latest +`AutomationState` before deciding whether to reapply its change. Omitted patch +fields remain unchanged; supplied arrays and objects replace their fields in +full rather than merging recursively. + +### Idempotent migration imports + +`createAutomation.import` carries a stable source, batch, and item identity for +legacy migration: + +```typescript +{ + source: 'legacy-client-store' + batchId: 'migration-2026-08-12' + itemId: 'automation-42' +} +``` + +Retrying an interrupted migration with the same identity resolves to the +already imported item rather than creating a duplicate. Migration should move +one definition to exactly one authority: + +1. Create the host definition disabled. +2. Verify the imported definition. +3. Remove the legacy copy from scheduler-visible storage. +4. Enable the host definition. + +Never leave both copies schedulable and never deduplicate definitions by +content; identical-looking automations may be intentional. + +## Runs + +`runAutomation` starts a manual run and returns its +`ahp-automation-run:` URI: + +```typescript +{ + channel: 'ahp-automation:/triage' + requestId: 'client-generated-idempotency-key' +} +``` + +`requestId` is durable. Retrying with the same automation and request id +returns the existing run URI, including after reconnect or an uncertain +response. The host persists the run record before creating sessions or sending +the first message. + +### Run lifecycle + +```mermaid +stateDiagram-v2 + [*] --> pending + pending --> running + pending --> failed + pending --> cancelled + running --> blocked + blocked --> running + running --> completed + running --> failed + running --> cancelled + blocked --> failed + blocked --> cancelled +``` + +`completed`, `failed`, and `cancelled` are terminal. `failed.startedAt` and +`cancelled.startedAt` are optional because validation, workspace preparation, +or cancellation may finish before execution begins. + +`blocked` is a coarse task-level summary: + +- `userInput` +- `toolConfirmation` +- `authentication` +- `clientExecution` + +The detailed prompt, confirmation, authentication request, or tool state lives +on a linked session channel. + +### Linked sessions + +A run's `sessions` list may contain one local attempt, several retries, or +parallel workers. `primarySession` tells clients which one to open first. + +Every automation-created session records: + +```typescript +origin: { + kind: 'automation' + automation: URI + run: URI +} ``` -The host is the single writable authority for definitions, trigger claims, -runs, and session links. Clients own presentation and confirmation UX. +This provenance survives persistence and allows navigation in both directions. +The run channel does not duplicate session transcript or tool state. + +### Artifacts + +`AutomationRunArtifact` represents output owned by the task as a whole rather +than one particular session. It extends `ContentRef`, so the client fetches +content through the normal resource APIs. Session-specific edits and outputs +remain on their session channels. + +### Cancellation + +Cancellation is available only when: + +1. `InitializeResult.automations.runCancellation` is present; and +2. the run's `operations` contains `cancel`. + +The client dispatches `automationRun/cancelRequested`. This action is +side-effect-only and does not optimistically change lifecycle. The host later +emits `automationRun/lifecycleChanged` with the authoritative result. The run +may become `cancelled`, or it may complete or fail before cancellation takes +effect. + +## Run history and retention + +`AutomationState.runs` is a newest-first, bounded window of +`AutomationRunSummary` values. If `runsNextCursor` is present, the client calls +`fetchAutomationRuns`; older summaries arrive through +`automation/runsLoaded` so every subscriber applies the same reducer update. + +`AutomationCapabilities.runHistoryLimit` advertises the maximum number of +terminal summaries retained per automation. Active runs do not count toward +that limit. Once the host prunes a run, clients must not assume its +automation-run channel remains subscribable. -## Manual and automatic triggers +## Multiple clients and reconciliation -An empty trigger collection is manual-only. Schedule triggers are portable -hourly, daily, weekly, or five-field cron schedules. Event triggers are -host-defined and retain unknown schema fields during edits. +Several clients may subscribe to the same authority: -## Multiple clients +- the host sequences every definition and run action; +- revisions prevent lost updates; +- manual `requestId` values prevent duplicate runs after retry; +- one trigger occurrence is atomically associated with at most one run; +- root catalogue notifications keep live clients updated; and +- reconnecting clients re-list and re-subscribe instead of replaying local + scheduling decisions. -Definitions use monotonic revisions. Manual run requests use durable -idempotency keys. Clients re-fetch catalogues after reconnect and never execute -a local fallback copy after an uncertain host response. +The core invariant is: -## Availability +> Once an automation belongs to an AHP authority, clients never schedule or +> execute a fallback copy. -`AutomationCapabilities.execution.lifetime` distinguishes a host-lifetime -scheduler from a managed service. This describes availability; it never moves -scheduling authority into clients. +This is what prevents duplicate runs across windows, devices, and +applications. -## Migration +## Security -Move one definition to one authority. Import the host copy disabled, remove the -legacy copy from scheduler-visible storage, then enable the host copy. Never -dual-schedule or deduplicate by content. +- Definitions contain no credentials or reusable confirmation decisions. +- The host revalidates provider, model, agent, workspace, and session + configuration when each run starts. +- State-level operations are authoritative; clients do not infer permission + from capabilities. +- Event provenance and `_meta` values must not contain secrets. +- Linked session channels use the ordinary AHP confirmation, authentication, + and client-execution mechanisms. diff --git a/docs/specification/automation-channel.md b/docs/specification/automation-channel.md index e005188ac..cd3f1a95a 100644 --- a/docs/specification/automation-channel.md +++ b/docs/specification/automation-channel.md @@ -18,8 +18,9 @@ revision ordering, trigger evaluation, run claims, and run history. host-computed next scheduled run, a newest-first window of `AutomationRunSummary` entries, and allowed operations. -An empty trigger list means manual-only. Schedule triggers are typed; event -triggers use a host-defined type plus schema-defined configuration returned by +An empty trigger list means manual-only. Schedule triggers use the portable +five-field AHP cron format plus an IANA time zone. Event triggers use a +host-defined type plus schema-defined configuration returned by `listAutomationTriggerDefinitions`. The session template can select a provider, model, and custom agent, and carries @@ -59,7 +60,14 @@ The host sequences all actions. Automation actions are server-originated. ## Scheduling -Scheduling belongs to the host. Calendar schedules persist an IANA time zone. +Scheduling belongs to the host. A schedule contains exactly five cron fields +(`minute hour day-of-month month day-of-week`) and an IANA time zone. The +portable grammar supports wildcards, values, inclusive ranges, +comma-separated lists, and steps over wildcards or ranges. It does not support +seconds, years, macros, or Quartz extensions. See the +[Automations guide](/guide/automations#scheduled-triggers) for the complete +grammar and day-field semantics. + `enabled` controls automatic triggers only; manual runs remain available when the operation is advertised. A host atomically associates a scheduled occurrence with at most one run. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index a93290b73..eea8329c4 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -2089,21 +2089,26 @@ }, "AutomationDefinitionChangedAction": { "type": "object", + "description": "Replace the editable definition after a successful `updateAutomation` or\nanother host-authorized definition change.\n\nFull replacement semantics apply to `definition`. The reducer also replaces\nthe revision and modification timestamp. Omitting `nextRunAt` clears the\npreviously projected next occurrence.", "properties": { "type": { "const": "automation/definitionChanged" }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Complete replacement definition." }, "revision": { - "type": "number" + "type": "number", + "description": "New monotonic revision." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Definition modification timestamp in ISO 8601 format." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, or omitted to clear it." } }, "required": [ @@ -2115,12 +2120,14 @@ }, "AutomationRunSummarySetAction": { "type": "object", + "description": "Upsert one run summary in the retained history.\n\nExisting entries are replaced by {@link AutomationRunSummary.resource}. A\npreviously unseen run is inserted at the front because history is\nnewest-first.", "properties": { "type": { "const": "automation/runSummarySet" }, "run": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "New or replacement run summary." } }, "required": [ @@ -2130,12 +2137,14 @@ }, "AutomationRunSummaryRemovedAction": { "type": "object", + "description": "Remove one retained run summary by its automation-run URI.\n\nThe action is a no-op when the URI is not present in the current history\nwindow.", "properties": { "type": { "const": "automation/runSummaryRemoved" }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "{@link AutomationRunSummary.resource} to remove." } }, "required": [ @@ -2145,6 +2154,7 @@ }, "AutomationRunsLoadedAction": { "type": "object", + "description": "Append an older page of run summaries returned by\n`fetchAutomationRuns`.\n\nEntries already present by resource URI are ignored, preserving the\nnewest-first ordering of the existing history followed by the fetched page.\nOmitting `nextCursor` marks the end of retained history.", "properties": { "type": { "const": "automation/runsLoaded" @@ -2153,10 +2163,12 @@ "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" - } + }, + "description": "Older run summaries in newest-first order within this page." }, "nextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older page, or omitted at the end." } }, "required": [ @@ -2166,18 +2178,21 @@ }, "AutomationRunLifecycleChangedAction": { "type": "object", + "description": "Replace the run lifecycle and currently allowed operations atomically.\n\nThe host dispatches this action for every lifecycle transition. Terminal\nlifecycles normally carry an empty operations list.", "properties": { "type": { "const": "automationRun/lifecycleChanged" }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Complete replacement lifecycle." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Complete replacement operation list." } }, "required": [ @@ -2188,12 +2203,14 @@ }, "AutomationRunSessionSetAction": { "type": "object", + "description": "Add a session to the run's ordered session catalogue.\n\nSession URIs are unique. Setting an existing URI is a no-op.", "properties": { "type": { "const": "automationRun/sessionSet" }, "session": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session URI to append when it is not already linked." } }, "required": [ @@ -2203,12 +2220,14 @@ }, "AutomationRunSessionRemovedAction": { "type": "object", + "description": "Remove a linked session from the run.\n\nRemoving the current primary session also clears\n{@link AutomationRunState.primarySession}. An unknown URI is a no-op.", "properties": { "type": { "const": "automationRun/sessionRemoved" }, "session": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Linked session URI to remove." } }, "required": [ @@ -2218,12 +2237,14 @@ }, "AutomationRunPrimarySessionChangedAction": { "type": "object", + "description": "Select or clear the session clients should open first for this run.", "properties": { "type": { "const": "automationRun/primarySessionChanged" }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "New primary linked session, or omitted to clear the selection." } }, "required": [ @@ -2232,12 +2253,14 @@ }, "AutomationRunArtifactSetAction": { "type": "object", + "description": "Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}.", "properties": { "type": { "const": "automationRun/artifactSet" }, "artifact": { - "$ref": "#/$defs/AutomationRunArtifact" + "$ref": "#/$defs/AutomationRunArtifact", + "description": "New or replacement artifact." } }, "required": [ @@ -2247,12 +2270,14 @@ }, "AutomationRunArtifactRemovedAction": { "type": "object", + "description": "Remove a run-scoped artifact by id.\n\nThe action is a no-op when the id is not present.", "properties": { "type": { "const": "automationRun/artifactRemoved" }, "artifactId": { - "type": "string" + "type": "string", + "description": "{@link AutomationRunArtifact.id} to remove." } }, "required": [ @@ -2262,6 +2287,7 @@ }, "AutomationRunCancelRequestedAction": { "type": "object", + "description": "Ask the host to cancel this run.\n\nThis is the only client-dispatchable automation-run action. It is a\nside-effect request and deliberately leaves optimistic state unchanged. The\nauthoritative outcome arrives later through\n{@link AutomationRunLifecycleChangedAction}: cancellation may transition to\n`cancelled`, or the run may complete or fail before cancellation takes\neffect.", "properties": { "type": { "const": "automationRun/cancelRequested" @@ -3021,15 +3047,18 @@ }, "AutomationSessionOrigin": { "type": "object", + "description": "Provenance recorded on a session created for an automation run.\n\nThe links let clients navigate from an ordinary session to the task-level\nrun and its durable definition. The session channel remains authoritative\nfor this session's transcript, tools, confirmations, and changes.", "properties": { "kind": { "const": "automation" }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` URI." } }, "required": [ @@ -7295,112 +7324,42 @@ "type" ] }, - "AutomationLocalTime": { - "type": "object", - "properties": { - "hour": { - "type": "number" - }, - "minute": { - "type": "number" - } - }, - "required": [ - "hour", - "minute" - ] - }, - "AutomationHourlySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "hourly" - } - }, - "required": [ - "kind" - ] - }, - "AutomationDailySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "daily" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "time", - "timeZone" - ] - }, - "AutomationWeeklySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "weekly" - }, - "weekday": { - "$ref": "#/$defs/AutomationWeekday" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "weekday", - "time", - "timeZone" - ] - }, - "AutomationCronSchedule": { + "AutomationSchedule": { "type": "object", + "description": "A portable recurring schedule evaluated in a named time zone.\n\nThe expression uses exactly five whitespace-separated fields, in this\norder:\n\n| Field | Values |\n| --- | --- |\n| minute | `0`–`59` |\n| hour | `0`–`23` |\n| day of month | `1`–`31` |\n| month | `1`–`12` or `JAN`–`DEC` |\n| day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday |\n\nMonth and weekday names are ASCII and case-insensitive. Each field accepts\n`*`, a single value, an inclusive range (`1-5`), a comma-separated list of\nvalues or ranges (`1,3,8-10`), or a step applied to `*` or a range (for\nexample, */15 or `1-30/2`). A step MUST be a positive integer. AHP does\nnot support seconds, years, macros such as `@daily`, or Quartz extensions\nsuch as `?`, `L`, `W`, and `#`.\n\nMinute, hour, and month must all match. When both day-of-month and\nday-of-week are restricted (not `*`), an occurrence matches when either day\nfield matches, following Unix cron semantics.", "properties": { - "kind": { - "const": "cron" - }, "expression": { "type": "string", - "description": "Standard five-field Unix cron expression." + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, "timeZone": { "type": "string", - "description": "IANA time-zone identifier." + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } }, "required": [ - "kind", "expression", "timeZone" ] }, "AutomationScheduleTrigger": { "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "schedule" }, "schedule": { - "$ref": "#/$defs/AutomationSchedule" + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." }, "misfirePolicy": { - "$ref": "#/$defs/AutomationMisfirePolicy" + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } }, "required": [ @@ -7411,29 +7370,30 @@ }, "AutomationEventTrigger": { "type": "object", + "description": "Starts runs from events understood by the owning host.\n\nEvent trigger types, event ids, and configuration are discovered through\n`listAutomationTriggerDefinitions`. A client that does not understand a\nhost-defined trigger can still preserve and display it without interpreting\nits configuration.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "event" }, "type": { "type": "string", - "description": "Stable host-defined trigger type." + "description": "Matches {@link AutomationTriggerDefinition.type}." }, "events": { "type": "array", "items": { "type": "string" }, - "description": "Selected event actions." + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Schema-defined values. Unknown entries must survive round-trips." + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } }, "required": [ @@ -7445,15 +7405,19 @@ }, "AutomationTriggerEventDefinition": { "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for selection UI." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of when this event fires." } }, "required": [ @@ -7463,24 +7427,30 @@ }, "AutomationTriggerDefinition": { "type": "object", + "description": "Describes one host-defined event trigger type available for a prospective\nautomation session template.\n\nTrigger definitions are discovery metadata, not durable automation state.\nHosts may return different definitions for different providers, working\ndirectories, or session configuration.", "properties": { "type": { - "type": "string" + "type": "string", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable trigger type name." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of the trigger source." }, "events": { "type": "array", "items": { "$ref": "#/$defs/AutomationTriggerEventDefinition" - } + }, + "description": "Events clients may select for this trigger type." }, "configSchema": { - "$ref": "#/$defs/ConfigSchema" + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } }, "required": [ @@ -7491,57 +7461,65 @@ }, "AutomationSessionTemplate": { "type": "object", + "description": "Template from which the host creates a fresh session for each automation run.\n\nThe host revalidates every selection when the run starts. Definitions never\ncarry credentials, confirmation decisions, or durable permission grants.", "properties": { "provider": { - "type": "string" + "type": "string", + "description": "Provider id. Omit to use the host's default provider." }, "model": { - "$ref": "#/$defs/ModelSelection" + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." }, "agent": { - "$ref": "#/$defs/AgentSelection" + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "Absence means a workspace-less session." + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Values resolved through `resolveSessionConfig`." + "description": "Session configuration values accepted by `createSession`, normally\nobtained from `resolveSessionConfig`." } } }, "AutomationDefinition": { "type": "object", + "description": "Durable, client-editable definition of an automation.\n\nA definition combines the initial user message, the session template used\nfor each run, and zero or more automatic triggers. Runtime state, run\nhistory, revisions, timestamps, and currently allowed operations live on\n{@link AutomationState} rather than in the definition.", "properties": { "title": { - "type": "string" + "type": "string", + "description": "Human-readable automation name." }, "message": { "$ref": "#/$defs/Message", - "description": "Initial user message sent to each new session." + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, "session": { - "$ref": "#/$defs/AutomationSessionTemplate" + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, "enabled": { "type": "boolean", - "description": "Controls automatic triggers; manual runs remain permitted." + "description": "Whether automatic triggers may create runs. Manual runs remain available\nwhenever {@link AutomationOperation.Run} is advertised." }, "triggers": { "type": "array", "items": { "$ref": "#/$defs/AutomationTrigger" }, - "description": "Empty means manual-only." + "description": "Automatic triggers. An empty list means manual-only." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." } }, "required": [ @@ -7554,58 +7532,73 @@ }, "AutomationRuntimeState": { "type": "object", + "description": "Host-resolved execution context that is useful to clients but is not part of\nthe editable definition.", "properties": { "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Effective working directories after host-side preparation, such as\nmaterializing a managed workspace." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined runtime metadata." } } }, "AutomationSummary": { "type": "object", + "description": "Lightweight root-catalogue projection of an automation.\n\nReturned by `listAutomations` and carried by root automation notifications,\nthis contains enough information to render a list without subscribing to\nevery `ahp-automation:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation:` URI." }, "title": { - "type": "string" + "type": "string", + "description": "Current {@link AutomationDefinition.title}." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." }, "triggerCount": { - "type": "number" + "type": "number", + "description": "Number of automatic triggers in the current definition." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "lastRun": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "Most recent retained run, when any run exists." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonic definition revision used for optimistic concurrency." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." } }, "required": [ @@ -7621,47 +7614,58 @@ }, "AutomationState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation:` resource.\n\nThe host owns definition revisions, trigger evaluation, run claims, run\nretention, and operation availability. Clients render this state and submit\ncommands; they never run a fallback scheduler for a host-owned definition.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation channel." }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "runs": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" }, - "description": "Newest-first retained run summaries." + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." }, "runsNextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older run-history page." }, "runtime": { - "$ref": "#/$defs/AutomationRuntimeState" + "$ref": "#/$defs/AutomationRuntimeState", + "description": "Optional host-resolved execution context." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." } }, "required": [ @@ -7676,9 +7680,11 @@ }, "AutomationRunBlocker": { "type": "object", + "description": "Summary of why a run cannot currently make progress.", "properties": { "kind": { - "$ref": "#/$defs/AutomationRunBlockerKind" + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." } }, "required": [ @@ -7687,6 +7693,7 @@ }, "AutomationManualRunCause": { "type": "object", + "description": "Cause recorded for a client-requested manual run.", "properties": { "kind": { "const": "manual" @@ -7698,23 +7705,27 @@ }, "AutomationTriggeredRunCause": { "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", "properties": { "kind": { "const": "trigger" }, "triggerId": { - "type": "string" + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." }, "scheduledFor": { - "type": "string" + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." }, "catchUp": { - "type": "boolean" + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." }, "event": { "type": "object", "additionalProperties": {}, - "description": "Host-defined event provenance containing no secrets." + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." } }, "required": [ @@ -7724,12 +7735,14 @@ }, "AutomationPendingRunLifecycle": { "type": "object", + "description": "A durable run exists but has not begun external execution.", "properties": { "status": { "const": "pending" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." } }, "required": [ @@ -7739,15 +7752,18 @@ }, "AutomationRunningRunLifecycle": { "type": "object", + "description": "The run is actively executing linked sessions.", "properties": { "status": { "const": "running" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." } }, "required": [ @@ -7758,18 +7774,22 @@ }, "AutomationBlockedRunLifecycle": { "type": "object", + "description": "The run started but is temporarily unable to progress.", "properties": { "status": { "const": "blocked" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "blocker": { - "$ref": "#/$defs/AutomationRunBlocker" + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." } }, "required": [ @@ -7781,21 +7801,26 @@ }, "AutomationCompletedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", "properties": { "status": { "const": "completed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Completion timestamp in ISO 8601 format." }, "usage": { - "$ref": "#/$defs/UsageInfo" + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." } }, "required": [ @@ -7807,21 +7832,26 @@ }, "AutomationFailedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a run that ended with an error.\n\n`startedAt` is absent when failure occurred before execution began, such as\nsession-template validation or workspace preparation.", "properties": { "status": { "const": "failed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Failure timestamp in ISO 8601 format." }, "error": { - "$ref": "#/$defs/ErrorInfo" + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." } }, "required": [ @@ -7833,18 +7863,22 @@ }, "AutomationCancelledRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a cancelled run.\n\n`startedAt` is absent when cancellation completed while the run was still\npending.", "properties": { "status": { "const": "cancelled" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." } }, "required": [ @@ -7855,6 +7889,7 @@ }, "AutomationRunArtifact": { "type": "object", + "description": "Fetchable output produced at run scope rather than by one specific session.\n\nThe inherited {@link ContentRef} identifies how the client obtains the\ncontent. Session-specific edits, transcripts, and tool results remain on\ntheir session and chat channels.", "properties": { "uri": { "$ref": "#/$defs/URI", @@ -7873,14 +7908,17 @@ "description": "Content nonce" }, "id": { - "type": "string" + "type": "string", + "description": "Stable artifact id within this run, used by artifact actions." }, "label": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for run-history UI." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." } }, "required": [ @@ -7891,37 +7929,47 @@ }, "AutomationRunSummary": { "type": "object", + "description": "Lightweight projection of a run retained in its automation's history.\n\nA summary contains enough information to render run history without\nsubscribing to every `ahp-automation-run:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "sessionCount": { - "type": "number" + "type": "number", + "description": "Number of linked sessions, including attempts and workers." }, "artifactCount": { - "type": "number" + "type": "number", + "description": "Number of run-scoped artifacts, when cheaply available." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined summary metadata." } }, "required": [ @@ -7935,43 +7983,53 @@ }, "AutomationRunState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation-run:` resource.\n\nThe run channel owns task-level lifecycle, provenance, linked-session\nmembership, artifacts, and cancellation availability. Linked session and\nchat channels remain authoritative for transcripts, tools, confirmations,\nchangesets, and per-session lifecycle.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation-run channel." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle." }, "sessions": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Ordered, unique session URIs belonging to this run. Entries may represent\nretries, parallel workers, or delegated attempts." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "artifacts": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunArtifact" - } + }, + "description": "Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined run metadata." } }, "required": [ @@ -8021,7 +8079,8 @@ "description": "A primitive JSON value: a string, number, boolean, or `null`." }, "SessionOrigin": { - "$ref": "#/$defs/AutomationSessionOrigin" + "$ref": "#/$defs/AutomationSessionOrigin", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." }, "SessionInputRequest": { "oneOf": [ @@ -8418,22 +8477,6 @@ ], "description": "A content part within terminal output." }, - "AutomationSchedule": { - "oneOf": [ - { - "$ref": "#/$defs/AutomationHourlySchedule" - }, - { - "$ref": "#/$defs/AutomationDailySchedule" - }, - { - "$ref": "#/$defs/AutomationWeeklySchedule" - }, - { - "$ref": "#/$defs/AutomationCronSchedule" - } - ] - }, "AutomationTrigger": { "oneOf": [ { @@ -8442,7 +8485,8 @@ { "$ref": "#/$defs/AutomationEventTrigger" } - ] + ], + "description": "An automatic cause that can create runs for an enabled automation.\n\nManual execution is not represented as a trigger. An empty trigger list\ntherefore means the automation is manual-only." }, "AutomationRunCause": { "oneOf": [ @@ -8452,7 +8496,8 @@ { "$ref": "#/$defs/AutomationTriggeredRunCause" } - ] + ], + "description": "Immutable provenance describing why a run was created." }, "AutomationRunLifecycle": { "oneOf": [ @@ -8474,7 +8519,8 @@ { "$ref": "#/$defs/AutomationCancelledRunLifecycle" } - ] + ], + "description": "Discriminated lifecycle of an automation run." }, "StateAction": { "description": "Discriminated union of all state actions.", @@ -8846,7 +8892,8 @@ "enum": [ "cancel" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for a run." }, "PolicyState": { "enum": [ @@ -8936,24 +8983,13 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, - "AutomationWeekday": { - "enum": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "type": "string" - }, "AutomationMisfirePolicy": { "enum": [ "skip", "runOnce" ], - "type": "string" + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." }, "AutomationOperation": { "enum": [ @@ -8961,7 +8997,8 @@ "dispose", "run" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for an automation.\n\nThe list on {@link AutomationState.operations} is authoritative and may\nchange over time. Clients MUST NOT infer permission from capabilities alone:\ncapabilities describe what the host implementation can support, while\noperations describe what is allowed for this particular automation now." }, "AutomationRunBlockerKind": { "enum": [ @@ -8970,7 +9007,8 @@ "authentication", "clientExecution" ], - "type": "string" + "type": "string", + "description": "Coarse reason a run is blocked.\n\nDetailed prompts, confirmations, authentication requests, and tool state\nremain authoritative on linked session and chat channels." } } } diff --git a/schema/commands.schema.json b/schema/commands.schema.json index b65c4987e..6d3c0154a 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -175,7 +175,7 @@ }, "automations": { "$ref": "#/$defs/AutomationCapabilities", - "description": "Host automation support. Absence means unsupported." + "description": "Host-owned automation support. Absence means the host does not expose an\nautomation catalogue or automation commands." } }, "required": [ @@ -186,24 +186,31 @@ }, "AutomationCapabilities": { "type": "object", + "description": "Automation features supported by this host authority.\n\nCapabilities describe implementation support. Per-resource\n{@link AutomationState.operations} and\n{@link AutomationRunState.operations} remain authoritative for whether a\nparticular operation is currently allowed.", "properties": { "execution": { - "$ref": "#/$defs/AutomationExecutionCapabilities" + "$ref": "#/$defs/AutomationExecutionCapabilities", + "description": "Availability guarantee for automatic trigger execution." }, "create": { - "$ref": "#/$defs/AutomationCreateCapability" + "$ref": "#/$defs/AutomationCreateCapability", + "description": "Present when clients may call `createAutomation`." }, "schedules": { - "$ref": "#/$defs/AutomationScheduleCapabilities" + "$ref": "#/$defs/AutomationScheduleCapabilities", + "description": "Present when definitions may contain schedule triggers." }, "runCancellation": { - "$ref": "#/$defs/AutomationRunCancellationCapability" + "$ref": "#/$defs/AutomationRunCancellationCapability", + "description": "Present when clients may request cancellation on eligible runs." }, "schedulePreview": { - "$ref": "#/$defs/AutomationSchedulePreviewCapability" + "$ref": "#/$defs/AutomationSchedulePreviewCapability", + "description": "Present when clients may call `previewAutomationSchedule`." }, "runHistoryLimit": { - "type": "number" + "type": "number", + "description": "Maximum terminal run summaries retained per automation. Active runs are not\ncounted toward the limit. Absence means the retention limit is\nimplementation-defined." } }, "required": [ @@ -212,9 +219,11 @@ }, "AutomationExecutionCapabilities": { "type": "object", + "description": "Automatic trigger execution availability.", "properties": { "lifetime": { - "$ref": "#/$defs/AutomationExecutionLifetime" + "$ref": "#/$defs/AutomationExecutionLifetime", + "description": "How long automatic trigger evaluation remains available." } }, "required": [ @@ -223,48 +232,27 @@ }, "AutomationCreateCapability": { "type": "object", + "description": "Presence capability for `createAutomation`.\n\nThe empty object means \"supported\"; fields are reserved for future\ncreate-specific options.", "properties": {} }, "AutomationScheduleCapabilities": { "type": "object", + "description": "Host restrictions on portable {@link AutomationSchedule} triggers.\n\nThe cron grammar itself is fixed by AHP. Hosts MUST accept every expression\nin that grammar unless it violates an advertised interval restriction.", "properties": { - "kinds": { - "type": "array", - "items": { - "$ref": "#/$defs/AutomationScheduleKind" - } - }, - "cron": { - "$ref": "#/$defs/AutomationCronScheduleCapability" - } - }, - "required": [ - "kinds" - ] - }, - "AutomationCronScheduleCapability": { - "type": "object", - "properties": { - "dialect": { - "type": "string", - "enum": [ - "unix5" - ] - }, "minIntervalMinutes": { - "type": "number" + "type": "number", + "description": "Smallest permitted interval between consecutive occurrences. Omission\nmeans no restriction beyond the cron format's one-minute resolution." } - }, - "required": [ - "dialect" - ] + } }, "AutomationRunCancellationCapability": { "type": "object", + "description": "Presence capability for `automationRun/cancelRequested`.\n\nThe empty object means \"supported\"; clients must additionally check for\n{@link AutomationRunOperation.Cancel} on each run.", "properties": {} }, "AutomationSchedulePreviewCapability": { "type": "object", + "description": "Presence capability for `previewAutomationSchedule`.\n\nThe empty object means \"supported\"; fields are reserved for future preview\nlimits or options.", "properties": {} }, "PingParams": { @@ -1587,12 +1575,14 @@ }, "ListAutomationsParams": { "type": "object", + "description": "List the host's automation catalogue without subscribing to every\nautomation channel.\n\nResults are lightweight {@link AutomationSummary} entries. Clients SHOULD\nre-run this command after reconnect because root catalogue notifications are\nnot replayed.", "properties": { "channel": { "type": "string", "enum": [ "ahp-root://" - ] + ], + "description": "Automation catalogues are listed from the root channel." }, "_meta": { "type": "object", @@ -1608,7 +1598,8 @@ "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Optional exact filter on {@link AutomationDefinition.enabled}." } }, "required": [ @@ -1617,6 +1608,7 @@ }, "ListAutomationsResult": { "type": "object", + "description": "One page of the automation catalogue.", "properties": { "nextCursor": { "type": "string", @@ -1626,7 +1618,8 @@ "type": "array", "items": { "$ref": "#/$defs/AutomationSummary" - } + }, + "description": "Automation summaries in host-defined catalogue order." } }, "required": [ @@ -1635,12 +1628,14 @@ }, "ListAutomationTriggerDefinitionsParams": { "type": "object", + "description": "Discover event-trigger types available for a prospective session template.\n\nHosts may vary definitions by provider, workspace, and session\nconfiguration. Schedule triggers are protocol-defined and therefore do not\nappear in this result.", "properties": { "channel": { "type": "string", "enum": [ "ahp-root://" - ] + ], + "description": "Trigger definitions are discovered from the root channel." }, "_meta": { "type": "object", @@ -1648,17 +1643,20 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "provider": { - "type": "string" + "type": "string", + "description": "Prospective provider id, or omitted for the host default." }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Prospective ordered working-directory list." }, "sessionConfig": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Prospective resolved session configuration values." } }, "required": [ @@ -1667,23 +1665,50 @@ }, "ListAutomationTriggerDefinitionsResult": { "type": "object", + "description": "Host-defined event trigger types available for the supplied context.", "properties": { "items": { "type": "array", "items": { "$ref": "#/$defs/AutomationTriggerDefinition" - } + }, + "description": "Available event trigger definitions." } }, "required": [ "items" ] }, + "AutomationImportIdentity": { + "type": "object", + "description": "Stable source identity used to make legacy automation import idempotent.\n\nThe host remembers this identity independently of the client-chosen\nautomation URI. Retrying an interrupted migration with the same values MUST\nresolve to the previously imported item rather than creating a duplicate.", + "properties": { + "source": { + "type": "string", + "description": "Stable namespace identifying the source implementation or store." + }, + "batchId": { + "type": "string", + "description": "Identifier shared by every item in one import attempt." + }, + "itemId": { + "type": "string", + "description": "Stable source-side identifier for this definition within the batch." + } + }, + "required": [ + "source", + "batchId", + "itemId" + ] + }, "CreateAutomationParams": { "type": "object", + "description": "Create a durable automation at a client-chosen URI.\n\n`channel` MUST use the `ahp-automation:` scheme and MUST NOT already identify\nan unrelated automation. The host validates the complete definition,\npersists it, and makes it visible through the root catalogue before\nreturning success.", "properties": { "channel": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Client-chosen `ahp-automation:` URI for the new definition." }, "_meta": { "type": "object", @@ -1691,26 +1716,12 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Complete initial definition." }, "import": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "batchId": { - "type": "string" - }, - "itemId": { - "type": "string" - } - }, - "required": [ - "source", - "batchId", - "itemId" - ] + "$ref": "#/$defs/AutomationImportIdentity", + "description": "Optional idempotency identity when importing a legacy definition." } }, "required": [ @@ -1720,36 +1731,45 @@ }, "AutomationDefinitionPatch": { "type": "object", + "description": "Partial replacement of editable {@link AutomationDefinition} fields.\n\nOmitted fields are unchanged. Supplied arrays and objects replace their\ncorresponding values in full; they are not merged recursively.", "properties": { "title": { - "type": "string" + "type": "string", + "description": "Replacement human-readable title." }, "message": { - "$ref": "#/$defs/Message" + "$ref": "#/$defs/Message", + "description": "Replacement initial user message." }, "session": { - "$ref": "#/$defs/AutomationSessionTemplate" + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Replacement session template." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Replacement automatic-trigger enabled state." }, "triggers": { "type": "array", "items": { "$ref": "#/$defs/AutomationTrigger" - } + }, + "description": "Complete replacement trigger list." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Complete replacement implementation-defined metadata." } } }, "UpdateAutomationParams": { "type": "object", + "description": "Update editable fields of an existing automation using optimistic\nconcurrency.\n\nThe host accepts the patch only when `expectedRevision` equals the current\n{@link AutomationState.revision}. A stale revision is rejected; clients\nSHOULD reconcile the latest state before retrying.", "properties": { "channel": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Target `ahp-automation:` URI." }, "_meta": { "type": "object", @@ -1757,10 +1777,12 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "expectedRevision": { - "type": "number" + "type": "number", + "description": "Revision on which the client based {@link changes}." }, "changes": { - "$ref": "#/$defs/AutomationDefinitionPatch" + "$ref": "#/$defs/AutomationDefinitionPatch", + "description": "Editable fields to replace." } }, "required": [ @@ -1771,6 +1793,7 @@ }, "DisposeAutomationParams": { "type": "object", + "description": "Permanently remove an automation.\n\nThe target is supplied by {@link BaseParams.channel}. The host rejects the\ncommand when {@link AutomationOperation.Dispose} is not currently\nadvertised, for example while a non-terminal run prevents disposal.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1788,6 +1811,7 @@ }, "RunAutomationParams": { "type": "object", + "description": "Start a manual run of an automation.\n\nManual execution is independent of {@link AutomationDefinition.enabled}.\nThe host persists the run before beginning session side effects.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1799,7 +1823,8 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "requestId": { - "type": "string" + "type": "string", + "description": "Durable client-generated idempotency key. Retrying with the same key and\nautomation MUST return the original run URI rather than create another\nrun." } }, "required": [ @@ -1809,9 +1834,11 @@ }, "RunAutomationResult": { "type": "object", + "description": "Result identifying the existing or newly created run.", "properties": { "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." } }, "required": [ @@ -1820,6 +1847,7 @@ }, "FetchAutomationRunsParams": { "type": "object", + "description": "Load one older page into the subscribed automation's run-history state.\n\nThe response only acknowledges the request. Loaded entries arrive through\n`automation/runsLoaded`, keeping all subscribers synchronized through the\nnormal action stream.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1831,7 +1859,8 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "cursor": { - "type": "string" + "type": "string", + "description": "Cursor previously received as {@link AutomationState.runsNextCursor}.\nOmit to request the first page not already included by the snapshot." } }, "required": [ @@ -1840,16 +1869,19 @@ }, "FetchAutomationRunsResult": { "type": "object", + "description": "Empty acknowledgement; run summaries are delivered by action.", "properties": {} }, "PreviewAutomationScheduleParams": { "type": "object", + "description": "Ask the host to evaluate a schedule without creating an automation.\n\nClients SHOULD use this command for validation and preview instead of\nimplementing their own cron evaluator, especially around time-zone\ntransitions.", "properties": { "channel": { "type": "string", "enum": [ "ahp-root://" - ] + ], + "description": "Schedule preview is requested from the root channel." }, "_meta": { "type": "object", @@ -1857,10 +1889,12 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "schedule": { - "$ref": "#/$defs/AutomationSchedule" + "$ref": "#/$defs/AutomationSchedule", + "description": "Portable AHP cron schedule to evaluate." }, "count": { - "type": "number" + "type": "number", + "description": "Requested maximum number of future occurrences; the host MAY cap it." } }, "required": [ @@ -1870,12 +1904,14 @@ }, "PreviewAutomationScheduleResult": { "type": "object", + "description": "Host-canonical future schedule occurrences.", "properties": { "items": { "type": "array", "items": { "type": "string" - } + }, + "description": "Ascending ISO 8601 timestamps." } }, "required": [ @@ -2530,15 +2566,18 @@ }, "AutomationSessionOrigin": { "type": "object", + "description": "Provenance recorded on a session created for an automation run.\n\nThe links let clients navigate from an ordinary session to the task-level\nrun and its durable definition. The session channel remains authoritative\nfor this session's transcript, tools, confirmations, and changes.", "properties": { "kind": { "const": "automation" }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` URI." } }, "required": [ @@ -6804,112 +6843,42 @@ "type" ] }, - "AutomationLocalTime": { - "type": "object", - "properties": { - "hour": { - "type": "number" - }, - "minute": { - "type": "number" - } - }, - "required": [ - "hour", - "minute" - ] - }, - "AutomationHourlySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "hourly" - } - }, - "required": [ - "kind" - ] - }, - "AutomationDailySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "daily" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "time", - "timeZone" - ] - }, - "AutomationWeeklySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "weekly" - }, - "weekday": { - "$ref": "#/$defs/AutomationWeekday" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "weekday", - "time", - "timeZone" - ] - }, - "AutomationCronSchedule": { + "AutomationSchedule": { "type": "object", + "description": "A portable recurring schedule evaluated in a named time zone.\n\nThe expression uses exactly five whitespace-separated fields, in this\norder:\n\n| Field | Values |\n| --- | --- |\n| minute | `0`–`59` |\n| hour | `0`–`23` |\n| day of month | `1`–`31` |\n| month | `1`–`12` or `JAN`–`DEC` |\n| day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday |\n\nMonth and weekday names are ASCII and case-insensitive. Each field accepts\n`*`, a single value, an inclusive range (`1-5`), a comma-separated list of\nvalues or ranges (`1,3,8-10`), or a step applied to `*` or a range (for\nexample, */15 or `1-30/2`). A step MUST be a positive integer. AHP does\nnot support seconds, years, macros such as `@daily`, or Quartz extensions\nsuch as `?`, `L`, `W`, and `#`.\n\nMinute, hour, and month must all match. When both day-of-month and\nday-of-week are restricted (not `*`), an occurrence matches when either day\nfield matches, following Unix cron semantics.", "properties": { - "kind": { - "const": "cron" - }, "expression": { "type": "string", - "description": "Standard five-field Unix cron expression." + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, "timeZone": { "type": "string", - "description": "IANA time-zone identifier." + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } }, "required": [ - "kind", "expression", "timeZone" ] }, "AutomationScheduleTrigger": { "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "schedule" }, "schedule": { - "$ref": "#/$defs/AutomationSchedule" + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." }, "misfirePolicy": { - "$ref": "#/$defs/AutomationMisfirePolicy" + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } }, "required": [ @@ -6920,29 +6889,30 @@ }, "AutomationEventTrigger": { "type": "object", + "description": "Starts runs from events understood by the owning host.\n\nEvent trigger types, event ids, and configuration are discovered through\n`listAutomationTriggerDefinitions`. A client that does not understand a\nhost-defined trigger can still preserve and display it without interpreting\nits configuration.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "event" }, "type": { "type": "string", - "description": "Stable host-defined trigger type." + "description": "Matches {@link AutomationTriggerDefinition.type}." }, "events": { "type": "array", "items": { "type": "string" }, - "description": "Selected event actions." + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Schema-defined values. Unknown entries must survive round-trips." + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } }, "required": [ @@ -6954,15 +6924,19 @@ }, "AutomationTriggerEventDefinition": { "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for selection UI." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of when this event fires." } }, "required": [ @@ -6972,24 +6946,30 @@ }, "AutomationTriggerDefinition": { "type": "object", + "description": "Describes one host-defined event trigger type available for a prospective\nautomation session template.\n\nTrigger definitions are discovery metadata, not durable automation state.\nHosts may return different definitions for different providers, working\ndirectories, or session configuration.", "properties": { "type": { - "type": "string" + "type": "string", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable trigger type name." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of the trigger source." }, "events": { "type": "array", "items": { "$ref": "#/$defs/AutomationTriggerEventDefinition" - } + }, + "description": "Events clients may select for this trigger type." }, "configSchema": { - "$ref": "#/$defs/ConfigSchema" + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } }, "required": [ @@ -7000,57 +6980,65 @@ }, "AutomationSessionTemplate": { "type": "object", + "description": "Template from which the host creates a fresh session for each automation run.\n\nThe host revalidates every selection when the run starts. Definitions never\ncarry credentials, confirmation decisions, or durable permission grants.", "properties": { "provider": { - "type": "string" + "type": "string", + "description": "Provider id. Omit to use the host's default provider." }, "model": { - "$ref": "#/$defs/ModelSelection" + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." }, "agent": { - "$ref": "#/$defs/AgentSelection" + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "Absence means a workspace-less session." + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Values resolved through `resolveSessionConfig`." + "description": "Session configuration values accepted by `createSession`, normally\nobtained from `resolveSessionConfig`." } } }, "AutomationDefinition": { "type": "object", + "description": "Durable, client-editable definition of an automation.\n\nA definition combines the initial user message, the session template used\nfor each run, and zero or more automatic triggers. Runtime state, run\nhistory, revisions, timestamps, and currently allowed operations live on\n{@link AutomationState} rather than in the definition.", "properties": { "title": { - "type": "string" + "type": "string", + "description": "Human-readable automation name." }, "message": { "$ref": "#/$defs/Message", - "description": "Initial user message sent to each new session." + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, "session": { - "$ref": "#/$defs/AutomationSessionTemplate" + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, "enabled": { "type": "boolean", - "description": "Controls automatic triggers; manual runs remain permitted." + "description": "Whether automatic triggers may create runs. Manual runs remain available\nwhenever {@link AutomationOperation.Run} is advertised." }, "triggers": { "type": "array", "items": { "$ref": "#/$defs/AutomationTrigger" }, - "description": "Empty means manual-only." + "description": "Automatic triggers. An empty list means manual-only." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." } }, "required": [ @@ -7063,58 +7051,73 @@ }, "AutomationRuntimeState": { "type": "object", + "description": "Host-resolved execution context that is useful to clients but is not part of\nthe editable definition.", "properties": { "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Effective working directories after host-side preparation, such as\nmaterializing a managed workspace." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined runtime metadata." } } }, "AutomationSummary": { "type": "object", + "description": "Lightweight root-catalogue projection of an automation.\n\nReturned by `listAutomations` and carried by root automation notifications,\nthis contains enough information to render a list without subscribing to\nevery `ahp-automation:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation:` URI." }, "title": { - "type": "string" + "type": "string", + "description": "Current {@link AutomationDefinition.title}." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." }, "triggerCount": { - "type": "number" + "type": "number", + "description": "Number of automatic triggers in the current definition." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "lastRun": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "Most recent retained run, when any run exists." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonic definition revision used for optimistic concurrency." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." } }, "required": [ @@ -7130,47 +7133,58 @@ }, "AutomationState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation:` resource.\n\nThe host owns definition revisions, trigger evaluation, run claims, run\nretention, and operation availability. Clients render this state and submit\ncommands; they never run a fallback scheduler for a host-owned definition.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation channel." }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "runs": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" }, - "description": "Newest-first retained run summaries." + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." }, "runsNextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older run-history page." }, "runtime": { - "$ref": "#/$defs/AutomationRuntimeState" + "$ref": "#/$defs/AutomationRuntimeState", + "description": "Optional host-resolved execution context." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." } }, "required": [ @@ -7185,9 +7199,11 @@ }, "AutomationRunBlocker": { "type": "object", + "description": "Summary of why a run cannot currently make progress.", "properties": { "kind": { - "$ref": "#/$defs/AutomationRunBlockerKind" + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." } }, "required": [ @@ -7196,6 +7212,7 @@ }, "AutomationManualRunCause": { "type": "object", + "description": "Cause recorded for a client-requested manual run.", "properties": { "kind": { "const": "manual" @@ -7207,23 +7224,27 @@ }, "AutomationTriggeredRunCause": { "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", "properties": { "kind": { "const": "trigger" }, "triggerId": { - "type": "string" + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." }, "scheduledFor": { - "type": "string" + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." }, "catchUp": { - "type": "boolean" + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." }, "event": { "type": "object", "additionalProperties": {}, - "description": "Host-defined event provenance containing no secrets." + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." } }, "required": [ @@ -7233,12 +7254,14 @@ }, "AutomationPendingRunLifecycle": { "type": "object", + "description": "A durable run exists but has not begun external execution.", "properties": { "status": { "const": "pending" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." } }, "required": [ @@ -7248,15 +7271,18 @@ }, "AutomationRunningRunLifecycle": { "type": "object", + "description": "The run is actively executing linked sessions.", "properties": { "status": { "const": "running" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." } }, "required": [ @@ -7267,18 +7293,22 @@ }, "AutomationBlockedRunLifecycle": { "type": "object", + "description": "The run started but is temporarily unable to progress.", "properties": { "status": { "const": "blocked" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "blocker": { - "$ref": "#/$defs/AutomationRunBlocker" + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." } }, "required": [ @@ -7290,21 +7320,26 @@ }, "AutomationCompletedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", "properties": { "status": { "const": "completed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Completion timestamp in ISO 8601 format." }, "usage": { - "$ref": "#/$defs/UsageInfo" + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." } }, "required": [ @@ -7316,21 +7351,26 @@ }, "AutomationFailedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a run that ended with an error.\n\n`startedAt` is absent when failure occurred before execution began, such as\nsession-template validation or workspace preparation.", "properties": { "status": { "const": "failed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Failure timestamp in ISO 8601 format." }, "error": { - "$ref": "#/$defs/ErrorInfo" + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." } }, "required": [ @@ -7342,18 +7382,22 @@ }, "AutomationCancelledRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a cancelled run.\n\n`startedAt` is absent when cancellation completed while the run was still\npending.", "properties": { "status": { "const": "cancelled" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." } }, "required": [ @@ -7364,6 +7408,7 @@ }, "AutomationRunArtifact": { "type": "object", + "description": "Fetchable output produced at run scope rather than by one specific session.\n\nThe inherited {@link ContentRef} identifies how the client obtains the\ncontent. Session-specific edits, transcripts, and tool results remain on\ntheir session and chat channels.", "properties": { "uri": { "$ref": "#/$defs/URI", @@ -7382,14 +7427,17 @@ "description": "Content nonce" }, "id": { - "type": "string" + "type": "string", + "description": "Stable artifact id within this run, used by artifact actions." }, "label": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for run-history UI." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." } }, "required": [ @@ -7400,37 +7448,47 @@ }, "AutomationRunSummary": { "type": "object", + "description": "Lightweight projection of a run retained in its automation's history.\n\nA summary contains enough information to render run history without\nsubscribing to every `ahp-automation-run:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "sessionCount": { - "type": "number" + "type": "number", + "description": "Number of linked sessions, including attempts and workers." }, "artifactCount": { - "type": "number" + "type": "number", + "description": "Number of run-scoped artifacts, when cheaply available." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined summary metadata." } }, "required": [ @@ -7444,43 +7502,53 @@ }, "AutomationRunState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation-run:` resource.\n\nThe run channel owns task-level lifecycle, provenance, linked-session\nmembership, artifacts, and cancellation availability. Linked session and\nchat channels remain authoritative for transcripts, tools, confirmations,\nchangesets, and per-session lifecycle.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation-run channel." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle." }, "sessions": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Ordered, unique session URIs belonging to this run. Entries may represent\nretries, parallel workers, or delegated attempts." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "artifacts": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunArtifact" - } + }, + "description": "Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined run metadata." } }, "required": [ @@ -9577,21 +9645,26 @@ }, "AutomationDefinitionChangedAction": { "type": "object", + "description": "Replace the editable definition after a successful `updateAutomation` or\nanother host-authorized definition change.\n\nFull replacement semantics apply to `definition`. The reducer also replaces\nthe revision and modification timestamp. Omitting `nextRunAt` clears the\npreviously projected next occurrence.", "properties": { "type": { "const": "automation/definitionChanged" }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Complete replacement definition." }, "revision": { - "type": "number" + "type": "number", + "description": "New monotonic revision." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Definition modification timestamp in ISO 8601 format." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, or omitted to clear it." } }, "required": [ @@ -9603,12 +9676,14 @@ }, "AutomationRunSummarySetAction": { "type": "object", + "description": "Upsert one run summary in the retained history.\n\nExisting entries are replaced by {@link AutomationRunSummary.resource}. A\npreviously unseen run is inserted at the front because history is\nnewest-first.", "properties": { "type": { "const": "automation/runSummarySet" }, "run": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "New or replacement run summary." } }, "required": [ @@ -9618,12 +9693,14 @@ }, "AutomationRunSummaryRemovedAction": { "type": "object", + "description": "Remove one retained run summary by its automation-run URI.\n\nThe action is a no-op when the URI is not present in the current history\nwindow.", "properties": { "type": { "const": "automation/runSummaryRemoved" }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "{@link AutomationRunSummary.resource} to remove." } }, "required": [ @@ -9633,6 +9710,7 @@ }, "AutomationRunsLoadedAction": { "type": "object", + "description": "Append an older page of run summaries returned by\n`fetchAutomationRuns`.\n\nEntries already present by resource URI are ignored, preserving the\nnewest-first ordering of the existing history followed by the fetched page.\nOmitting `nextCursor` marks the end of retained history.", "properties": { "type": { "const": "automation/runsLoaded" @@ -9641,10 +9719,12 @@ "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" - } + }, + "description": "Older run summaries in newest-first order within this page." }, "nextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older page, or omitted at the end." } }, "required": [ @@ -9654,18 +9734,21 @@ }, "AutomationRunLifecycleChangedAction": { "type": "object", + "description": "Replace the run lifecycle and currently allowed operations atomically.\n\nThe host dispatches this action for every lifecycle transition. Terminal\nlifecycles normally carry an empty operations list.", "properties": { "type": { "const": "automationRun/lifecycleChanged" }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Complete replacement lifecycle." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Complete replacement operation list." } }, "required": [ @@ -9676,12 +9759,14 @@ }, "AutomationRunSessionSetAction": { "type": "object", + "description": "Add a session to the run's ordered session catalogue.\n\nSession URIs are unique. Setting an existing URI is a no-op.", "properties": { "type": { "const": "automationRun/sessionSet" }, "session": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session URI to append when it is not already linked." } }, "required": [ @@ -9691,12 +9776,14 @@ }, "AutomationRunSessionRemovedAction": { "type": "object", + "description": "Remove a linked session from the run.\n\nRemoving the current primary session also clears\n{@link AutomationRunState.primarySession}. An unknown URI is a no-op.", "properties": { "type": { "const": "automationRun/sessionRemoved" }, "session": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Linked session URI to remove." } }, "required": [ @@ -9706,12 +9793,14 @@ }, "AutomationRunPrimarySessionChangedAction": { "type": "object", + "description": "Select or clear the session clients should open first for this run.", "properties": { "type": { "const": "automationRun/primarySessionChanged" }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "New primary linked session, or omitted to clear the selection." } }, "required": [ @@ -9720,12 +9809,14 @@ }, "AutomationRunArtifactSetAction": { "type": "object", + "description": "Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}.", "properties": { "type": { "const": "automationRun/artifactSet" }, "artifact": { - "$ref": "#/$defs/AutomationRunArtifact" + "$ref": "#/$defs/AutomationRunArtifact", + "description": "New or replacement artifact." } }, "required": [ @@ -9735,12 +9826,14 @@ }, "AutomationRunArtifactRemovedAction": { "type": "object", + "description": "Remove a run-scoped artifact by id.\n\nThe action is a no-op when the id is not present.", "properties": { "type": { "const": "automationRun/artifactRemoved" }, "artifactId": { - "type": "string" + "type": "string", + "description": "{@link AutomationRunArtifact.id} to remove." } }, "required": [ @@ -9750,6 +9843,7 @@ }, "AutomationRunCancelRequestedAction": { "type": "object", + "description": "Ask the host to cancel this run.\n\nThis is the only client-dispatchable automation-run action. It is a\nside-effect request and deliberately leaves optimistic state unchanged. The\nauthoritative outcome arrives later through\n{@link AutomationRunLifecycleChangedAction}: cancellation may transition to\n`cancelled`, or the run may complete or fail before cancellation takes\neffect.", "properties": { "type": { "const": "automationRun/cancelRequested" @@ -9768,16 +9862,8 @@ "hostLifetime", "managed" ], - "type": "string" - }, - "AutomationScheduleKind": { - "enum": [ - "hourly", - "daily", - "weekly", - "cron" - ], - "type": "string" + "type": "string", + "description": "Availability guarantee for host-owned automatic trigger evaluation.\n\nThis describes the authority that owns one automation catalogue. It does not\nprevent a client from connecting to several authorities with different\nlifetimes (for example, one local host and one managed service)." }, "StateAction": { "oneOf": [ @@ -10227,23 +10313,8 @@ { "$ref": "#/$defs/AutomationEventTrigger" } - ] - }, - "AutomationSchedule": { - "oneOf": [ - { - "$ref": "#/$defs/AutomationHourlySchedule" - }, - { - "$ref": "#/$defs/AutomationDailySchedule" - }, - { - "$ref": "#/$defs/AutomationWeeklySchedule" - }, - { - "$ref": "#/$defs/AutomationCronSchedule" - } - ] + ], + "description": "An automatic cause that can create runs for an enabled automation.\n\nManual execution is not represented as a trigger. An empty trigger list\ntherefore means the automation is manual-only." }, "JsonPrimitive": { "oneOf": [ @@ -10298,7 +10369,8 @@ "description": "Bitset of summary-level session status flags.\n\nUse bitwise checks instead of equality for non-terminal activity. For example,\n`status & SessionStatus.InProgress` matches both ordinary in-progress turns\nand turns that are paused waiting for input." }, "SessionOrigin": { - "$ref": "#/$defs/AutomationSessionOrigin" + "$ref": "#/$defs/AutomationSessionOrigin", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." }, "SessionLifecycle": { "enum": [ @@ -10775,24 +10847,13 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, - "AutomationWeekday": { - "enum": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "type": "string" - }, "AutomationMisfirePolicy": { "enum": [ "skip", "runOnce" ], - "type": "string" + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." }, "AutomationOperation": { "enum": [ @@ -10800,7 +10861,8 @@ "dispose", "run" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for an automation.\n\nThe list on {@link AutomationState.operations} is authoritative and may\nchange over time. Clients MUST NOT infer permission from capabilities alone:\ncapabilities describe what the host implementation can support, while\noperations describe what is allowed for this particular automation now." }, "AutomationRunBlockerKind": { "enum": [ @@ -10809,7 +10871,8 @@ "authentication", "clientExecution" ], - "type": "string" + "type": "string", + "description": "Coarse reason a run is blocked.\n\nDetailed prompts, confirmations, authentication requests, and tool state\nremain authoritative on linked session and chat channels." }, "AutomationRunCause": { "oneOf": [ @@ -10819,7 +10882,8 @@ { "$ref": "#/$defs/AutomationTriggeredRunCause" } - ] + ], + "description": "Immutable provenance describing why a run was created." }, "AutomationRunLifecycle": { "oneOf": [ @@ -10841,13 +10905,15 @@ { "$ref": "#/$defs/AutomationCancelledRunLifecycle" } - ] + ], + "description": "Discriminated lifecycle of an automation run." }, "AutomationRunOperation": { "enum": [ "cancel" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for a run." }, "PendingMessageKind": { "enum": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index db5ac9489..8ac50f0d6 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -742,15 +742,18 @@ }, "AutomationSessionOrigin": { "type": "object", + "description": "Provenance recorded on a session created for an automation run.\n\nThe links let clients navigate from an ordinary session to the task-level\nrun and its durable definition. The session channel remains authoritative\nfor this session's transcript, tools, confirmations, and changes.", "properties": { "kind": { "const": "automation" }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` URI." } }, "required": [ @@ -5016,112 +5019,42 @@ "type" ] }, - "AutomationLocalTime": { - "type": "object", - "properties": { - "hour": { - "type": "number" - }, - "minute": { - "type": "number" - } - }, - "required": [ - "hour", - "minute" - ] - }, - "AutomationHourlySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "hourly" - } - }, - "required": [ - "kind" - ] - }, - "AutomationDailySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "daily" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "time", - "timeZone" - ] - }, - "AutomationWeeklySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "weekly" - }, - "weekday": { - "$ref": "#/$defs/AutomationWeekday" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "weekday", - "time", - "timeZone" - ] - }, - "AutomationCronSchedule": { + "AutomationSchedule": { "type": "object", + "description": "A portable recurring schedule evaluated in a named time zone.\n\nThe expression uses exactly five whitespace-separated fields, in this\norder:\n\n| Field | Values |\n| --- | --- |\n| minute | `0`–`59` |\n| hour | `0`–`23` |\n| day of month | `1`–`31` |\n| month | `1`–`12` or `JAN`–`DEC` |\n| day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday |\n\nMonth and weekday names are ASCII and case-insensitive. Each field accepts\n`*`, a single value, an inclusive range (`1-5`), a comma-separated list of\nvalues or ranges (`1,3,8-10`), or a step applied to `*` or a range (for\nexample, */15 or `1-30/2`). A step MUST be a positive integer. AHP does\nnot support seconds, years, macros such as `@daily`, or Quartz extensions\nsuch as `?`, `L`, `W`, and `#`.\n\nMinute, hour, and month must all match. When both day-of-month and\nday-of-week are restricted (not `*`), an occurrence matches when either day\nfield matches, following Unix cron semantics.", "properties": { - "kind": { - "const": "cron" - }, "expression": { "type": "string", - "description": "Standard five-field Unix cron expression." + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, "timeZone": { "type": "string", - "description": "IANA time-zone identifier." + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } }, "required": [ - "kind", "expression", "timeZone" ] }, "AutomationScheduleTrigger": { "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "schedule" }, "schedule": { - "$ref": "#/$defs/AutomationSchedule" + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." }, "misfirePolicy": { - "$ref": "#/$defs/AutomationMisfirePolicy" + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } }, "required": [ @@ -5132,29 +5065,30 @@ }, "AutomationEventTrigger": { "type": "object", + "description": "Starts runs from events understood by the owning host.\n\nEvent trigger types, event ids, and configuration are discovered through\n`listAutomationTriggerDefinitions`. A client that does not understand a\nhost-defined trigger can still preserve and display it without interpreting\nits configuration.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "event" }, "type": { "type": "string", - "description": "Stable host-defined trigger type." + "description": "Matches {@link AutomationTriggerDefinition.type}." }, "events": { "type": "array", "items": { "type": "string" }, - "description": "Selected event actions." + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Schema-defined values. Unknown entries must survive round-trips." + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } }, "required": [ @@ -5166,15 +5100,19 @@ }, "AutomationTriggerEventDefinition": { "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for selection UI." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of when this event fires." } }, "required": [ @@ -5184,24 +5122,30 @@ }, "AutomationTriggerDefinition": { "type": "object", + "description": "Describes one host-defined event trigger type available for a prospective\nautomation session template.\n\nTrigger definitions are discovery metadata, not durable automation state.\nHosts may return different definitions for different providers, working\ndirectories, or session configuration.", "properties": { "type": { - "type": "string" + "type": "string", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable trigger type name." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of the trigger source." }, "events": { "type": "array", "items": { "$ref": "#/$defs/AutomationTriggerEventDefinition" - } + }, + "description": "Events clients may select for this trigger type." }, "configSchema": { - "$ref": "#/$defs/ConfigSchema" + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } }, "required": [ @@ -5212,57 +5156,65 @@ }, "AutomationSessionTemplate": { "type": "object", + "description": "Template from which the host creates a fresh session for each automation run.\n\nThe host revalidates every selection when the run starts. Definitions never\ncarry credentials, confirmation decisions, or durable permission grants.", "properties": { "provider": { - "type": "string" + "type": "string", + "description": "Provider id. Omit to use the host's default provider." }, "model": { - "$ref": "#/$defs/ModelSelection" + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." }, "agent": { - "$ref": "#/$defs/AgentSelection" + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "Absence means a workspace-less session." + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Values resolved through `resolveSessionConfig`." + "description": "Session configuration values accepted by `createSession`, normally\nobtained from `resolveSessionConfig`." } } }, "AutomationDefinition": { "type": "object", + "description": "Durable, client-editable definition of an automation.\n\nA definition combines the initial user message, the session template used\nfor each run, and zero or more automatic triggers. Runtime state, run\nhistory, revisions, timestamps, and currently allowed operations live on\n{@link AutomationState} rather than in the definition.", "properties": { "title": { - "type": "string" + "type": "string", + "description": "Human-readable automation name." }, "message": { "$ref": "#/$defs/Message", - "description": "Initial user message sent to each new session." + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, "session": { - "$ref": "#/$defs/AutomationSessionTemplate" + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, "enabled": { "type": "boolean", - "description": "Controls automatic triggers; manual runs remain permitted." + "description": "Whether automatic triggers may create runs. Manual runs remain available\nwhenever {@link AutomationOperation.Run} is advertised." }, "triggers": { "type": "array", "items": { "$ref": "#/$defs/AutomationTrigger" }, - "description": "Empty means manual-only." + "description": "Automatic triggers. An empty list means manual-only." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." } }, "required": [ @@ -5275,58 +5227,73 @@ }, "AutomationRuntimeState": { "type": "object", + "description": "Host-resolved execution context that is useful to clients but is not part of\nthe editable definition.", "properties": { "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Effective working directories after host-side preparation, such as\nmaterializing a managed workspace." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined runtime metadata." } } }, "AutomationSummary": { "type": "object", + "description": "Lightweight root-catalogue projection of an automation.\n\nReturned by `listAutomations` and carried by root automation notifications,\nthis contains enough information to render a list without subscribing to\nevery `ahp-automation:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation:` URI." }, "title": { - "type": "string" + "type": "string", + "description": "Current {@link AutomationDefinition.title}." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." }, "triggerCount": { - "type": "number" + "type": "number", + "description": "Number of automatic triggers in the current definition." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "lastRun": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "Most recent retained run, when any run exists." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonic definition revision used for optimistic concurrency." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." } }, "required": [ @@ -5342,47 +5309,58 @@ }, "AutomationState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation:` resource.\n\nThe host owns definition revisions, trigger evaluation, run claims, run\nretention, and operation availability. Clients render this state and submit\ncommands; they never run a fallback scheduler for a host-owned definition.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation channel." }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "runs": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" }, - "description": "Newest-first retained run summaries." + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." }, "runsNextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older run-history page." }, "runtime": { - "$ref": "#/$defs/AutomationRuntimeState" + "$ref": "#/$defs/AutomationRuntimeState", + "description": "Optional host-resolved execution context." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." } }, "required": [ @@ -5397,9 +5375,11 @@ }, "AutomationRunBlocker": { "type": "object", + "description": "Summary of why a run cannot currently make progress.", "properties": { "kind": { - "$ref": "#/$defs/AutomationRunBlockerKind" + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." } }, "required": [ @@ -5408,6 +5388,7 @@ }, "AutomationManualRunCause": { "type": "object", + "description": "Cause recorded for a client-requested manual run.", "properties": { "kind": { "const": "manual" @@ -5419,23 +5400,27 @@ }, "AutomationTriggeredRunCause": { "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", "properties": { "kind": { "const": "trigger" }, "triggerId": { - "type": "string" + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." }, "scheduledFor": { - "type": "string" + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." }, "catchUp": { - "type": "boolean" + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." }, "event": { "type": "object", "additionalProperties": {}, - "description": "Host-defined event provenance containing no secrets." + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." } }, "required": [ @@ -5445,12 +5430,14 @@ }, "AutomationPendingRunLifecycle": { "type": "object", + "description": "A durable run exists but has not begun external execution.", "properties": { "status": { "const": "pending" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." } }, "required": [ @@ -5460,15 +5447,18 @@ }, "AutomationRunningRunLifecycle": { "type": "object", + "description": "The run is actively executing linked sessions.", "properties": { "status": { "const": "running" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." } }, "required": [ @@ -5479,18 +5469,22 @@ }, "AutomationBlockedRunLifecycle": { "type": "object", + "description": "The run started but is temporarily unable to progress.", "properties": { "status": { "const": "blocked" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "blocker": { - "$ref": "#/$defs/AutomationRunBlocker" + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." } }, "required": [ @@ -5502,21 +5496,26 @@ }, "AutomationCompletedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", "properties": { "status": { "const": "completed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Completion timestamp in ISO 8601 format." }, "usage": { - "$ref": "#/$defs/UsageInfo" + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." } }, "required": [ @@ -5528,21 +5527,26 @@ }, "AutomationFailedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a run that ended with an error.\n\n`startedAt` is absent when failure occurred before execution began, such as\nsession-template validation or workspace preparation.", "properties": { "status": { "const": "failed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Failure timestamp in ISO 8601 format." }, "error": { - "$ref": "#/$defs/ErrorInfo" + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." } }, "required": [ @@ -5554,18 +5558,22 @@ }, "AutomationCancelledRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a cancelled run.\n\n`startedAt` is absent when cancellation completed while the run was still\npending.", "properties": { "status": { "const": "cancelled" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." } }, "required": [ @@ -5576,6 +5584,7 @@ }, "AutomationRunArtifact": { "type": "object", + "description": "Fetchable output produced at run scope rather than by one specific session.\n\nThe inherited {@link ContentRef} identifies how the client obtains the\ncontent. Session-specific edits, transcripts, and tool results remain on\ntheir session and chat channels.", "properties": { "uri": { "$ref": "#/$defs/URI", @@ -5594,14 +5603,17 @@ "description": "Content nonce" }, "id": { - "type": "string" + "type": "string", + "description": "Stable artifact id within this run, used by artifact actions." }, "label": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for run-history UI." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." } }, "required": [ @@ -5612,37 +5624,47 @@ }, "AutomationRunSummary": { "type": "object", + "description": "Lightweight projection of a run retained in its automation's history.\n\nA summary contains enough information to render run history without\nsubscribing to every `ahp-automation-run:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "sessionCount": { - "type": "number" + "type": "number", + "description": "Number of linked sessions, including attempts and workers." }, "artifactCount": { - "type": "number" + "type": "number", + "description": "Number of run-scoped artifacts, when cheaply available." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined summary metadata." } }, "required": [ @@ -5656,43 +5678,53 @@ }, "AutomationRunState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation-run:` resource.\n\nThe run channel owns task-level lifecycle, provenance, linked-session\nmembership, artifacts, and cancellation availability. Linked session and\nchat channels remain authoritative for transcripts, tools, confirmations,\nchangesets, and per-session lifecycle.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation-run channel." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle." }, "sessions": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Ordered, unique session URIs belonging to this run. Entries may represent\nretries, parallel workers, or delegated attempts." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "artifacts": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunArtifact" - } + }, + "description": "Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined run metadata." } }, "required": [ @@ -5875,7 +5907,7 @@ }, "automations": { "$ref": "#/$defs/AutomationCapabilities", - "description": "Host automation support. Absence means unsupported." + "description": "Host-owned automation support. Absence means the host does not expose an\nautomation catalogue or automation commands." } }, "required": [ @@ -5886,24 +5918,31 @@ }, "AutomationCapabilities": { "type": "object", + "description": "Automation features supported by this host authority.\n\nCapabilities describe implementation support. Per-resource\n{@link AutomationState.operations} and\n{@link AutomationRunState.operations} remain authoritative for whether a\nparticular operation is currently allowed.", "properties": { "execution": { - "$ref": "#/$defs/AutomationExecutionCapabilities" + "$ref": "#/$defs/AutomationExecutionCapabilities", + "description": "Availability guarantee for automatic trigger execution." }, "create": { - "$ref": "#/$defs/AutomationCreateCapability" + "$ref": "#/$defs/AutomationCreateCapability", + "description": "Present when clients may call `createAutomation`." }, "schedules": { - "$ref": "#/$defs/AutomationScheduleCapabilities" + "$ref": "#/$defs/AutomationScheduleCapabilities", + "description": "Present when definitions may contain schedule triggers." }, "runCancellation": { - "$ref": "#/$defs/AutomationRunCancellationCapability" + "$ref": "#/$defs/AutomationRunCancellationCapability", + "description": "Present when clients may request cancellation on eligible runs." }, "schedulePreview": { - "$ref": "#/$defs/AutomationSchedulePreviewCapability" + "$ref": "#/$defs/AutomationSchedulePreviewCapability", + "description": "Present when clients may call `previewAutomationSchedule`." }, "runHistoryLimit": { - "type": "number" + "type": "number", + "description": "Maximum terminal run summaries retained per automation. Active runs are not\ncounted toward the limit. Absence means the retention limit is\nimplementation-defined." } }, "required": [ @@ -5912,9 +5951,11 @@ }, "AutomationExecutionCapabilities": { "type": "object", + "description": "Automatic trigger execution availability.", "properties": { "lifetime": { - "$ref": "#/$defs/AutomationExecutionLifetime" + "$ref": "#/$defs/AutomationExecutionLifetime", + "description": "How long automatic trigger evaluation remains available." } }, "required": [ @@ -5923,48 +5964,27 @@ }, "AutomationCreateCapability": { "type": "object", + "description": "Presence capability for `createAutomation`.\n\nThe empty object means \"supported\"; fields are reserved for future\ncreate-specific options.", "properties": {} }, "AutomationScheduleCapabilities": { "type": "object", + "description": "Host restrictions on portable {@link AutomationSchedule} triggers.\n\nThe cron grammar itself is fixed by AHP. Hosts MUST accept every expression\nin that grammar unless it violates an advertised interval restriction.", "properties": { - "kinds": { - "type": "array", - "items": { - "$ref": "#/$defs/AutomationScheduleKind" - } - }, - "cron": { - "$ref": "#/$defs/AutomationCronScheduleCapability" - } - }, - "required": [ - "kinds" - ] - }, - "AutomationCronScheduleCapability": { - "type": "object", - "properties": { - "dialect": { - "type": "string", - "enum": [ - "unix5" - ] - }, "minIntervalMinutes": { - "type": "number" + "type": "number", + "description": "Smallest permitted interval between consecutive occurrences. Omission\nmeans no restriction beyond the cron format's one-minute resolution." } - }, - "required": [ - "dialect" - ] + } }, "AutomationRunCancellationCapability": { "type": "object", + "description": "Presence capability for `automationRun/cancelRequested`.\n\nThe empty object means \"supported\"; clients must additionally check for\n{@link AutomationRunOperation.Cancel} on each run.", "properties": {} }, "AutomationSchedulePreviewCapability": { "type": "object", + "description": "Presence capability for `previewAutomationSchedule`.\n\nThe empty object means \"supported\"; fields are reserved for future preview\nlimits or options.", "properties": {} }, "PingParams": { @@ -7287,12 +7307,14 @@ }, "ListAutomationsParams": { "type": "object", + "description": "List the host's automation catalogue without subscribing to every\nautomation channel.\n\nResults are lightweight {@link AutomationSummary} entries. Clients SHOULD\nre-run this command after reconnect because root catalogue notifications are\nnot replayed.", "properties": { "channel": { "type": "string", "enum": [ "ahp-root://" - ] + ], + "description": "Automation catalogues are listed from the root channel." }, "_meta": { "type": "object", @@ -7308,7 +7330,8 @@ "description": "Opaque pagination cursor from a previous {@link PaginatedResult.nextCursor}.\nOmit to fetch the first page. Cursors are server-defined and MUST be treated\nas opaque — do not parse, modify, or persist them across connections. An\nunrecognised cursor SHOULD be rejected with an `InvalidParams` error." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Optional exact filter on {@link AutomationDefinition.enabled}." } }, "required": [ @@ -7317,6 +7340,7 @@ }, "ListAutomationsResult": { "type": "object", + "description": "One page of the automation catalogue.", "properties": { "nextCursor": { "type": "string", @@ -7326,7 +7350,8 @@ "type": "array", "items": { "$ref": "#/$defs/AutomationSummary" - } + }, + "description": "Automation summaries in host-defined catalogue order." } }, "required": [ @@ -7335,12 +7360,14 @@ }, "ListAutomationTriggerDefinitionsParams": { "type": "object", + "description": "Discover event-trigger types available for a prospective session template.\n\nHosts may vary definitions by provider, workspace, and session\nconfiguration. Schedule triggers are protocol-defined and therefore do not\nappear in this result.", "properties": { "channel": { "type": "string", "enum": [ "ahp-root://" - ] + ], + "description": "Trigger definitions are discovered from the root channel." }, "_meta": { "type": "object", @@ -7348,17 +7375,20 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "provider": { - "type": "string" + "type": "string", + "description": "Prospective provider id, or omitted for the host default." }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Prospective ordered working-directory list." }, "sessionConfig": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Prospective resolved session configuration values." } }, "required": [ @@ -7367,23 +7397,50 @@ }, "ListAutomationTriggerDefinitionsResult": { "type": "object", + "description": "Host-defined event trigger types available for the supplied context.", "properties": { "items": { "type": "array", "items": { "$ref": "#/$defs/AutomationTriggerDefinition" - } + }, + "description": "Available event trigger definitions." } }, "required": [ "items" ] }, + "AutomationImportIdentity": { + "type": "object", + "description": "Stable source identity used to make legacy automation import idempotent.\n\nThe host remembers this identity independently of the client-chosen\nautomation URI. Retrying an interrupted migration with the same values MUST\nresolve to the previously imported item rather than creating a duplicate.", + "properties": { + "source": { + "type": "string", + "description": "Stable namespace identifying the source implementation or store." + }, + "batchId": { + "type": "string", + "description": "Identifier shared by every item in one import attempt." + }, + "itemId": { + "type": "string", + "description": "Stable source-side identifier for this definition within the batch." + } + }, + "required": [ + "source", + "batchId", + "itemId" + ] + }, "CreateAutomationParams": { "type": "object", + "description": "Create a durable automation at a client-chosen URI.\n\n`channel` MUST use the `ahp-automation:` scheme and MUST NOT already identify\nan unrelated automation. The host validates the complete definition,\npersists it, and makes it visible through the root catalogue before\nreturning success.", "properties": { "channel": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Client-chosen `ahp-automation:` URI for the new definition." }, "_meta": { "type": "object", @@ -7391,26 +7448,12 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Complete initial definition." }, "import": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "batchId": { - "type": "string" - }, - "itemId": { - "type": "string" - } - }, - "required": [ - "source", - "batchId", - "itemId" - ] + "$ref": "#/$defs/AutomationImportIdentity", + "description": "Optional idempotency identity when importing a legacy definition." } }, "required": [ @@ -7420,36 +7463,45 @@ }, "AutomationDefinitionPatch": { "type": "object", + "description": "Partial replacement of editable {@link AutomationDefinition} fields.\n\nOmitted fields are unchanged. Supplied arrays and objects replace their\ncorresponding values in full; they are not merged recursively.", "properties": { "title": { - "type": "string" + "type": "string", + "description": "Replacement human-readable title." }, "message": { - "$ref": "#/$defs/Message" + "$ref": "#/$defs/Message", + "description": "Replacement initial user message." }, "session": { - "$ref": "#/$defs/AutomationSessionTemplate" + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Replacement session template." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Replacement automatic-trigger enabled state." }, "triggers": { "type": "array", "items": { "$ref": "#/$defs/AutomationTrigger" - } + }, + "description": "Complete replacement trigger list." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Complete replacement implementation-defined metadata." } } }, "UpdateAutomationParams": { "type": "object", + "description": "Update editable fields of an existing automation using optimistic\nconcurrency.\n\nThe host accepts the patch only when `expectedRevision` equals the current\n{@link AutomationState.revision}. A stale revision is rejected; clients\nSHOULD reconcile the latest state before retrying.", "properties": { "channel": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Target `ahp-automation:` URI." }, "_meta": { "type": "object", @@ -7457,10 +7509,12 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "expectedRevision": { - "type": "number" + "type": "number", + "description": "Revision on which the client based {@link changes}." }, "changes": { - "$ref": "#/$defs/AutomationDefinitionPatch" + "$ref": "#/$defs/AutomationDefinitionPatch", + "description": "Editable fields to replace." } }, "required": [ @@ -7471,6 +7525,7 @@ }, "DisposeAutomationParams": { "type": "object", + "description": "Permanently remove an automation.\n\nThe target is supplied by {@link BaseParams.channel}. The host rejects the\ncommand when {@link AutomationOperation.Dispose} is not currently\nadvertised, for example while a non-terminal run prevents disposal.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -7488,6 +7543,7 @@ }, "RunAutomationParams": { "type": "object", + "description": "Start a manual run of an automation.\n\nManual execution is independent of {@link AutomationDefinition.enabled}.\nThe host persists the run before beginning session side effects.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -7499,7 +7555,8 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "requestId": { - "type": "string" + "type": "string", + "description": "Durable client-generated idempotency key. Retrying with the same key and\nautomation MUST return the original run URI rather than create another\nrun." } }, "required": [ @@ -7509,9 +7566,11 @@ }, "RunAutomationResult": { "type": "object", + "description": "Result identifying the existing or newly created run.", "properties": { "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." } }, "required": [ @@ -7520,6 +7579,7 @@ }, "FetchAutomationRunsParams": { "type": "object", + "description": "Load one older page into the subscribed automation's run-history state.\n\nThe response only acknowledges the request. Loaded entries arrive through\n`automation/runsLoaded`, keeping all subscribers synchronized through the\nnormal action stream.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -7531,7 +7591,8 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "cursor": { - "type": "string" + "type": "string", + "description": "Cursor previously received as {@link AutomationState.runsNextCursor}.\nOmit to request the first page not already included by the snapshot." } }, "required": [ @@ -7540,16 +7601,19 @@ }, "FetchAutomationRunsResult": { "type": "object", + "description": "Empty acknowledgement; run summaries are delivered by action.", "properties": {} }, "PreviewAutomationScheduleParams": { "type": "object", + "description": "Ask the host to evaluate a schedule without creating an automation.\n\nClients SHOULD use this command for validation and preview instead of\nimplementing their own cron evaluator, especially around time-zone\ntransitions.", "properties": { "channel": { "type": "string", "enum": [ "ahp-root://" - ] + ], + "description": "Schedule preview is requested from the root channel." }, "_meta": { "type": "object", @@ -7557,10 +7621,12 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "schedule": { - "$ref": "#/$defs/AutomationSchedule" + "$ref": "#/$defs/AutomationSchedule", + "description": "Portable AHP cron schedule to evaluate." }, "count": { - "type": "number" + "type": "number", + "description": "Requested maximum number of future occurrences; the host MAY cap it." } }, "required": [ @@ -7570,12 +7636,14 @@ }, "PreviewAutomationScheduleResult": { "type": "object", + "description": "Host-canonical future schedule occurrences.", "properties": { "items": { "type": "array", "items": { "type": "string" - } + }, + "description": "Ascending ISO 8601 timestamps." } }, "required": [ @@ -7639,7 +7707,8 @@ "description": "Bitset of summary-level session status flags.\n\nUse bitwise checks instead of equality for non-terminal activity. For example,\n`status & SessionStatus.InProgress` matches both ordinary in-progress turns\nand turns that are paused waiting for input." }, "SessionOrigin": { - "$ref": "#/$defs/AutomationSessionOrigin" + "$ref": "#/$defs/AutomationSessionOrigin", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." }, "SessionLifecycle": { "enum": [ @@ -8166,40 +8235,13 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, - "AutomationWeekday": { - "enum": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "type": "string" - }, - "AutomationSchedule": { - "oneOf": [ - { - "$ref": "#/$defs/AutomationHourlySchedule" - }, - { - "$ref": "#/$defs/AutomationDailySchedule" - }, - { - "$ref": "#/$defs/AutomationWeeklySchedule" - }, - { - "$ref": "#/$defs/AutomationCronSchedule" - } - ] - }, "AutomationMisfirePolicy": { "enum": [ "skip", "runOnce" ], - "type": "string" + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." }, "AutomationTrigger": { "oneOf": [ @@ -8209,7 +8251,8 @@ { "$ref": "#/$defs/AutomationEventTrigger" } - ] + ], + "description": "An automatic cause that can create runs for an enabled automation.\n\nManual execution is not represented as a trigger. An empty trigger list\ntherefore means the automation is manual-only." }, "AutomationOperation": { "enum": [ @@ -8217,7 +8260,8 @@ "dispose", "run" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for an automation.\n\nThe list on {@link AutomationState.operations} is authoritative and may\nchange over time. Clients MUST NOT infer permission from capabilities alone:\ncapabilities describe what the host implementation can support, while\noperations describe what is allowed for this particular automation now." }, "AutomationRunBlockerKind": { "enum": [ @@ -8226,7 +8270,8 @@ "authentication", "clientExecution" ], - "type": "string" + "type": "string", + "description": "Coarse reason a run is blocked.\n\nDetailed prompts, confirmations, authentication requests, and tool state\nremain authoritative on linked session and chat channels." }, "AutomationRunCause": { "oneOf": [ @@ -8236,7 +8281,8 @@ { "$ref": "#/$defs/AutomationTriggeredRunCause" } - ] + ], + "description": "Immutable provenance describing why a run was created." }, "AutomationRunLifecycle": { "oneOf": [ @@ -8258,29 +8304,23 @@ { "$ref": "#/$defs/AutomationCancelledRunLifecycle" } - ] + ], + "description": "Discriminated lifecycle of an automation run." }, "AutomationRunOperation": { "enum": [ "cancel" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for a run." }, "AutomationExecutionLifetime": { "enum": [ "hostLifetime", "managed" ], - "type": "string" - }, - "AutomationScheduleKind": { - "enum": [ - "hourly", - "daily", - "weekly", - "cron" - ], - "type": "string" + "type": "string", + "description": "Availability guarantee for host-owned automatic trigger evaluation.\n\nThis describes the authority that owns one automation catalogue. It does not\nprevent a client from connecting to several authorities with different\nlifetimes (for example, one local host and one managed service)." }, "ActionEnvelope": { "type": "object", @@ -10645,21 +10685,26 @@ }, "AutomationDefinitionChangedAction": { "type": "object", + "description": "Replace the editable definition after a successful `updateAutomation` or\nanother host-authorized definition change.\n\nFull replacement semantics apply to `definition`. The reducer also replaces\nthe revision and modification timestamp. Omitting `nextRunAt` clears the\npreviously projected next occurrence.", "properties": { "type": { "const": "automation/definitionChanged" }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Complete replacement definition." }, "revision": { - "type": "number" + "type": "number", + "description": "New monotonic revision." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Definition modification timestamp in ISO 8601 format." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, or omitted to clear it." } }, "required": [ @@ -10671,12 +10716,14 @@ }, "AutomationRunSummarySetAction": { "type": "object", + "description": "Upsert one run summary in the retained history.\n\nExisting entries are replaced by {@link AutomationRunSummary.resource}. A\npreviously unseen run is inserted at the front because history is\nnewest-first.", "properties": { "type": { "const": "automation/runSummarySet" }, "run": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "New or replacement run summary." } }, "required": [ @@ -10686,12 +10733,14 @@ }, "AutomationRunSummaryRemovedAction": { "type": "object", + "description": "Remove one retained run summary by its automation-run URI.\n\nThe action is a no-op when the URI is not present in the current history\nwindow.", "properties": { "type": { "const": "automation/runSummaryRemoved" }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "{@link AutomationRunSummary.resource} to remove." } }, "required": [ @@ -10701,6 +10750,7 @@ }, "AutomationRunsLoadedAction": { "type": "object", + "description": "Append an older page of run summaries returned by\n`fetchAutomationRuns`.\n\nEntries already present by resource URI are ignored, preserving the\nnewest-first ordering of the existing history followed by the fetched page.\nOmitting `nextCursor` marks the end of retained history.", "properties": { "type": { "const": "automation/runsLoaded" @@ -10709,10 +10759,12 @@ "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" - } + }, + "description": "Older run summaries in newest-first order within this page." }, "nextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older page, or omitted at the end." } }, "required": [ @@ -10722,18 +10774,21 @@ }, "AutomationRunLifecycleChangedAction": { "type": "object", + "description": "Replace the run lifecycle and currently allowed operations atomically.\n\nThe host dispatches this action for every lifecycle transition. Terminal\nlifecycles normally carry an empty operations list.", "properties": { "type": { "const": "automationRun/lifecycleChanged" }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Complete replacement lifecycle." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Complete replacement operation list." } }, "required": [ @@ -10744,12 +10799,14 @@ }, "AutomationRunSessionSetAction": { "type": "object", + "description": "Add a session to the run's ordered session catalogue.\n\nSession URIs are unique. Setting an existing URI is a no-op.", "properties": { "type": { "const": "automationRun/sessionSet" }, "session": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session URI to append when it is not already linked." } }, "required": [ @@ -10759,12 +10816,14 @@ }, "AutomationRunSessionRemovedAction": { "type": "object", + "description": "Remove a linked session from the run.\n\nRemoving the current primary session also clears\n{@link AutomationRunState.primarySession}. An unknown URI is a no-op.", "properties": { "type": { "const": "automationRun/sessionRemoved" }, "session": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Linked session URI to remove." } }, "required": [ @@ -10774,12 +10833,14 @@ }, "AutomationRunPrimarySessionChangedAction": { "type": "object", + "description": "Select or clear the session clients should open first for this run.", "properties": { "type": { "const": "automationRun/primarySessionChanged" }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "New primary linked session, or omitted to clear the selection." } }, "required": [ @@ -10788,12 +10849,14 @@ }, "AutomationRunArtifactSetAction": { "type": "object", + "description": "Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}.", "properties": { "type": { "const": "automationRun/artifactSet" }, "artifact": { - "$ref": "#/$defs/AutomationRunArtifact" + "$ref": "#/$defs/AutomationRunArtifact", + "description": "New or replacement artifact." } }, "required": [ @@ -10803,12 +10866,14 @@ }, "AutomationRunArtifactRemovedAction": { "type": "object", + "description": "Remove a run-scoped artifact by id.\n\nThe action is a no-op when the id is not present.", "properties": { "type": { "const": "automationRun/artifactRemoved" }, "artifactId": { - "type": "string" + "type": "string", + "description": "{@link AutomationRunArtifact.id} to remove." } }, "required": [ @@ -10818,6 +10883,7 @@ }, "AutomationRunCancelRequestedAction": { "type": "object", + "description": "Ask the host to cancel this run.\n\nThis is the only client-dispatchable automation-run action. It is a\nside-effect request and deliberately leaves optimistic state unchanged. The\nauthoritative outcome arrives later through\n{@link AutomationRunLifecycleChangedAction}: cancellation may transition to\n`cancelled`, or the run may complete or fail before cancellation takes\neffect.", "properties": { "type": { "const": "automationRun/cancelRequested" diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index b573a1741..97799a86b 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -146,12 +146,15 @@ }, "AutomationAddedParams": { "type": "object", + "description": "Announces a newly visible automation catalogue entry.\n\nRoot notifications are live signals and are not replayed after reconnect.\nClients that reconnect MUST refresh the catalogue with `listAutomations`.", "properties": { "channel": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Root channel URI." }, "summary": { - "$ref": "#/$defs/AutomationSummary" + "$ref": "#/$defs/AutomationSummary", + "description": "Complete summary for the newly visible automation." } }, "required": [ @@ -161,12 +164,15 @@ }, "AutomationRemovedParams": { "type": "object", + "description": "Announces that an automation is no longer present in the root catalogue.", "properties": { "channel": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Root channel URI." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Removed `ahp-automation:` URI." } }, "required": [ @@ -176,12 +182,15 @@ }, "AutomationSummaryChangedParams": { "type": "object", + "description": "Replaces the root-catalogue summary for an existing automation.\n\nFull replacement semantics apply to `summary`; this is not a patch. The\ncorresponding subscribed automation channel remains authoritative.", "properties": { "channel": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Root channel URI." }, "summary": { - "$ref": "#/$defs/AutomationSummary" + "$ref": "#/$defs/AutomationSummary", + "description": "Complete replacement catalogue summary." } }, "required": [ @@ -963,15 +972,18 @@ }, "AutomationSessionOrigin": { "type": "object", + "description": "Provenance recorded on a session created for an automation run.\n\nThe links let clients navigate from an ordinary session to the task-level\nrun and its durable definition. The session channel remains authoritative\nfor this session's transcript, tools, confirmations, and changes.", "properties": { "kind": { "const": "automation" }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` URI." } }, "required": [ @@ -5237,112 +5249,42 @@ "type" ] }, - "AutomationLocalTime": { - "type": "object", - "properties": { - "hour": { - "type": "number" - }, - "minute": { - "type": "number" - } - }, - "required": [ - "hour", - "minute" - ] - }, - "AutomationHourlySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "hourly" - } - }, - "required": [ - "kind" - ] - }, - "AutomationDailySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "daily" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "time", - "timeZone" - ] - }, - "AutomationWeeklySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "weekly" - }, - "weekday": { - "$ref": "#/$defs/AutomationWeekday" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "weekday", - "time", - "timeZone" - ] - }, - "AutomationCronSchedule": { + "AutomationSchedule": { "type": "object", + "description": "A portable recurring schedule evaluated in a named time zone.\n\nThe expression uses exactly five whitespace-separated fields, in this\norder:\n\n| Field | Values |\n| --- | --- |\n| minute | `0`–`59` |\n| hour | `0`–`23` |\n| day of month | `1`–`31` |\n| month | `1`–`12` or `JAN`–`DEC` |\n| day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday |\n\nMonth and weekday names are ASCII and case-insensitive. Each field accepts\n`*`, a single value, an inclusive range (`1-5`), a comma-separated list of\nvalues or ranges (`1,3,8-10`), or a step applied to `*` or a range (for\nexample, */15 or `1-30/2`). A step MUST be a positive integer. AHP does\nnot support seconds, years, macros such as `@daily`, or Quartz extensions\nsuch as `?`, `L`, `W`, and `#`.\n\nMinute, hour, and month must all match. When both day-of-month and\nday-of-week are restricted (not `*`), an occurrence matches when either day\nfield matches, following Unix cron semantics.", "properties": { - "kind": { - "const": "cron" - }, "expression": { "type": "string", - "description": "Standard five-field Unix cron expression." + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, "timeZone": { "type": "string", - "description": "IANA time-zone identifier." + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } }, "required": [ - "kind", "expression", "timeZone" ] }, "AutomationScheduleTrigger": { "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "schedule" }, "schedule": { - "$ref": "#/$defs/AutomationSchedule" + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." }, "misfirePolicy": { - "$ref": "#/$defs/AutomationMisfirePolicy" + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } }, "required": [ @@ -5353,29 +5295,30 @@ }, "AutomationEventTrigger": { "type": "object", + "description": "Starts runs from events understood by the owning host.\n\nEvent trigger types, event ids, and configuration are discovered through\n`listAutomationTriggerDefinitions`. A client that does not understand a\nhost-defined trigger can still preserve and display it without interpreting\nits configuration.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "event" }, "type": { "type": "string", - "description": "Stable host-defined trigger type." + "description": "Matches {@link AutomationTriggerDefinition.type}." }, "events": { "type": "array", "items": { "type": "string" }, - "description": "Selected event actions." + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Schema-defined values. Unknown entries must survive round-trips." + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } }, "required": [ @@ -5387,15 +5330,19 @@ }, "AutomationTriggerEventDefinition": { "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for selection UI." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of when this event fires." } }, "required": [ @@ -5405,24 +5352,30 @@ }, "AutomationTriggerDefinition": { "type": "object", + "description": "Describes one host-defined event trigger type available for a prospective\nautomation session template.\n\nTrigger definitions are discovery metadata, not durable automation state.\nHosts may return different definitions for different providers, working\ndirectories, or session configuration.", "properties": { "type": { - "type": "string" + "type": "string", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable trigger type name." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of the trigger source." }, "events": { "type": "array", "items": { "$ref": "#/$defs/AutomationTriggerEventDefinition" - } + }, + "description": "Events clients may select for this trigger type." }, "configSchema": { - "$ref": "#/$defs/ConfigSchema" + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } }, "required": [ @@ -5433,57 +5386,65 @@ }, "AutomationSessionTemplate": { "type": "object", + "description": "Template from which the host creates a fresh session for each automation run.\n\nThe host revalidates every selection when the run starts. Definitions never\ncarry credentials, confirmation decisions, or durable permission grants.", "properties": { "provider": { - "type": "string" + "type": "string", + "description": "Provider id. Omit to use the host's default provider." }, "model": { - "$ref": "#/$defs/ModelSelection" + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." }, "agent": { - "$ref": "#/$defs/AgentSelection" + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "Absence means a workspace-less session." + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Values resolved through `resolveSessionConfig`." + "description": "Session configuration values accepted by `createSession`, normally\nobtained from `resolveSessionConfig`." } } }, "AutomationDefinition": { "type": "object", + "description": "Durable, client-editable definition of an automation.\n\nA definition combines the initial user message, the session template used\nfor each run, and zero or more automatic triggers. Runtime state, run\nhistory, revisions, timestamps, and currently allowed operations live on\n{@link AutomationState} rather than in the definition.", "properties": { "title": { - "type": "string" + "type": "string", + "description": "Human-readable automation name." }, "message": { "$ref": "#/$defs/Message", - "description": "Initial user message sent to each new session." + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, "session": { - "$ref": "#/$defs/AutomationSessionTemplate" + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, "enabled": { "type": "boolean", - "description": "Controls automatic triggers; manual runs remain permitted." + "description": "Whether automatic triggers may create runs. Manual runs remain available\nwhenever {@link AutomationOperation.Run} is advertised." }, "triggers": { "type": "array", "items": { "$ref": "#/$defs/AutomationTrigger" }, - "description": "Empty means manual-only." + "description": "Automatic triggers. An empty list means manual-only." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." } }, "required": [ @@ -5496,58 +5457,73 @@ }, "AutomationRuntimeState": { "type": "object", + "description": "Host-resolved execution context that is useful to clients but is not part of\nthe editable definition.", "properties": { "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Effective working directories after host-side preparation, such as\nmaterializing a managed workspace." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined runtime metadata." } } }, "AutomationSummary": { "type": "object", + "description": "Lightweight root-catalogue projection of an automation.\n\nReturned by `listAutomations` and carried by root automation notifications,\nthis contains enough information to render a list without subscribing to\nevery `ahp-automation:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation:` URI." }, "title": { - "type": "string" + "type": "string", + "description": "Current {@link AutomationDefinition.title}." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." }, "triggerCount": { - "type": "number" + "type": "number", + "description": "Number of automatic triggers in the current definition." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "lastRun": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "Most recent retained run, when any run exists." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonic definition revision used for optimistic concurrency." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." } }, "required": [ @@ -5563,47 +5539,58 @@ }, "AutomationState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation:` resource.\n\nThe host owns definition revisions, trigger evaluation, run claims, run\nretention, and operation availability. Clients render this state and submit\ncommands; they never run a fallback scheduler for a host-owned definition.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation channel." }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "runs": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" }, - "description": "Newest-first retained run summaries." + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." }, "runsNextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older run-history page." }, "runtime": { - "$ref": "#/$defs/AutomationRuntimeState" + "$ref": "#/$defs/AutomationRuntimeState", + "description": "Optional host-resolved execution context." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." } }, "required": [ @@ -5618,9 +5605,11 @@ }, "AutomationRunBlocker": { "type": "object", + "description": "Summary of why a run cannot currently make progress.", "properties": { "kind": { - "$ref": "#/$defs/AutomationRunBlockerKind" + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." } }, "required": [ @@ -5629,6 +5618,7 @@ }, "AutomationManualRunCause": { "type": "object", + "description": "Cause recorded for a client-requested manual run.", "properties": { "kind": { "const": "manual" @@ -5640,23 +5630,27 @@ }, "AutomationTriggeredRunCause": { "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", "properties": { "kind": { "const": "trigger" }, "triggerId": { - "type": "string" + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." }, "scheduledFor": { - "type": "string" + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." }, "catchUp": { - "type": "boolean" + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." }, "event": { "type": "object", "additionalProperties": {}, - "description": "Host-defined event provenance containing no secrets." + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." } }, "required": [ @@ -5666,12 +5660,14 @@ }, "AutomationPendingRunLifecycle": { "type": "object", + "description": "A durable run exists but has not begun external execution.", "properties": { "status": { "const": "pending" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." } }, "required": [ @@ -5681,15 +5677,18 @@ }, "AutomationRunningRunLifecycle": { "type": "object", + "description": "The run is actively executing linked sessions.", "properties": { "status": { "const": "running" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." } }, "required": [ @@ -5700,18 +5699,22 @@ }, "AutomationBlockedRunLifecycle": { "type": "object", + "description": "The run started but is temporarily unable to progress.", "properties": { "status": { "const": "blocked" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "blocker": { - "$ref": "#/$defs/AutomationRunBlocker" + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." } }, "required": [ @@ -5723,21 +5726,26 @@ }, "AutomationCompletedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", "properties": { "status": { "const": "completed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Completion timestamp in ISO 8601 format." }, "usage": { - "$ref": "#/$defs/UsageInfo" + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." } }, "required": [ @@ -5749,21 +5757,26 @@ }, "AutomationFailedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a run that ended with an error.\n\n`startedAt` is absent when failure occurred before execution began, such as\nsession-template validation or workspace preparation.", "properties": { "status": { "const": "failed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Failure timestamp in ISO 8601 format." }, "error": { - "$ref": "#/$defs/ErrorInfo" + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." } }, "required": [ @@ -5775,18 +5788,22 @@ }, "AutomationCancelledRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a cancelled run.\n\n`startedAt` is absent when cancellation completed while the run was still\npending.", "properties": { "status": { "const": "cancelled" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." } }, "required": [ @@ -5797,6 +5814,7 @@ }, "AutomationRunArtifact": { "type": "object", + "description": "Fetchable output produced at run scope rather than by one specific session.\n\nThe inherited {@link ContentRef} identifies how the client obtains the\ncontent. Session-specific edits, transcripts, and tool results remain on\ntheir session and chat channels.", "properties": { "uri": { "$ref": "#/$defs/URI", @@ -5815,14 +5833,17 @@ "description": "Content nonce" }, "id": { - "type": "string" + "type": "string", + "description": "Stable artifact id within this run, used by artifact actions." }, "label": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for run-history UI." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." } }, "required": [ @@ -5833,37 +5854,47 @@ }, "AutomationRunSummary": { "type": "object", + "description": "Lightweight projection of a run retained in its automation's history.\n\nA summary contains enough information to render run history without\nsubscribing to every `ahp-automation-run:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "sessionCount": { - "type": "number" + "type": "number", + "description": "Number of linked sessions, including attempts and workers." }, "artifactCount": { - "type": "number" + "type": "number", + "description": "Number of run-scoped artifacts, when cheaply available." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined summary metadata." } }, "required": [ @@ -5877,43 +5908,53 @@ }, "AutomationRunState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation-run:` resource.\n\nThe run channel owns task-level lifecycle, provenance, linked-session\nmembership, artifacts, and cancellation availability. Linked session and\nchat channels remain authoritative for transcripts, tools, confirmations,\nchangesets, and per-session lifecycle.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation-run channel." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle." }, "sessions": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Ordered, unique session URIs belonging to this run. Entries may represent\nretries, parallel workers, or delegated attempts." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "artifacts": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunArtifact" - } + }, + "description": "Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined run metadata." } }, "required": [ @@ -5951,7 +5992,8 @@ "description": "Bitset of summary-level session status flags.\n\nUse bitwise checks instead of equality for non-terminal activity. For example,\n`status & SessionStatus.InProgress` matches both ordinary in-progress turns\nand turns that are paused waiting for input." }, "SessionOrigin": { - "$ref": "#/$defs/AutomationSessionOrigin" + "$ref": "#/$defs/AutomationSessionOrigin", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." }, "JsonPrimitive": { "oneOf": [ @@ -6518,40 +6560,13 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, - "AutomationWeekday": { - "enum": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "type": "string" - }, - "AutomationSchedule": { - "oneOf": [ - { - "$ref": "#/$defs/AutomationHourlySchedule" - }, - { - "$ref": "#/$defs/AutomationDailySchedule" - }, - { - "$ref": "#/$defs/AutomationWeeklySchedule" - }, - { - "$ref": "#/$defs/AutomationCronSchedule" - } - ] - }, "AutomationMisfirePolicy": { "enum": [ "skip", "runOnce" ], - "type": "string" + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." }, "AutomationTrigger": { "oneOf": [ @@ -6561,7 +6576,8 @@ { "$ref": "#/$defs/AutomationEventTrigger" } - ] + ], + "description": "An automatic cause that can create runs for an enabled automation.\n\nManual execution is not represented as a trigger. An empty trigger list\ntherefore means the automation is manual-only." }, "AutomationOperation": { "enum": [ @@ -6569,7 +6585,8 @@ "dispose", "run" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for an automation.\n\nThe list on {@link AutomationState.operations} is authoritative and may\nchange over time. Clients MUST NOT infer permission from capabilities alone:\ncapabilities describe what the host implementation can support, while\noperations describe what is allowed for this particular automation now." }, "AutomationRunBlockerKind": { "enum": [ @@ -6578,7 +6595,8 @@ "authentication", "clientExecution" ], - "type": "string" + "type": "string", + "description": "Coarse reason a run is blocked.\n\nDetailed prompts, confirmations, authentication requests, and tool state\nremain authoritative on linked session and chat channels." }, "AutomationRunCause": { "oneOf": [ @@ -6588,7 +6606,8 @@ { "$ref": "#/$defs/AutomationTriggeredRunCause" } - ] + ], + "description": "Immutable provenance describing why a run was created." }, "AutomationRunLifecycle": { "oneOf": [ @@ -6610,13 +6629,15 @@ { "$ref": "#/$defs/AutomationCancelledRunLifecycle" } - ] + ], + "description": "Discriminated lifecycle of an automation run." }, "AutomationRunOperation": { "enum": [ "cancel" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for a run." } } } diff --git a/schema/state.schema.json b/schema/state.schema.json index 1226c99f0..7f0e1de69 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -653,15 +653,18 @@ }, "AutomationSessionOrigin": { "type": "object", + "description": "Provenance recorded on a session created for an automation run.\n\nThe links let clients navigate from an ordinary session to the task-level\nrun and its durable definition. The session channel remains authoritative\nfor this session's transcript, tools, confirmations, and changes.", "properties": { "kind": { "const": "automation" }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "run": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` URI." } }, "required": [ @@ -4927,112 +4930,42 @@ "type" ] }, - "AutomationLocalTime": { - "type": "object", - "properties": { - "hour": { - "type": "number" - }, - "minute": { - "type": "number" - } - }, - "required": [ - "hour", - "minute" - ] - }, - "AutomationHourlySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "hourly" - } - }, - "required": [ - "kind" - ] - }, - "AutomationDailySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "daily" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "time", - "timeZone" - ] - }, - "AutomationWeeklySchedule": { - "type": "object", - "properties": { - "kind": { - "const": "weekly" - }, - "weekday": { - "$ref": "#/$defs/AutomationWeekday" - }, - "time": { - "$ref": "#/$defs/AutomationLocalTime" - }, - "timeZone": { - "type": "string", - "description": "IANA time-zone identifier." - } - }, - "required": [ - "kind", - "weekday", - "time", - "timeZone" - ] - }, - "AutomationCronSchedule": { + "AutomationSchedule": { "type": "object", + "description": "A portable recurring schedule evaluated in a named time zone.\n\nThe expression uses exactly five whitespace-separated fields, in this\norder:\n\n| Field | Values |\n| --- | --- |\n| minute | `0`–`59` |\n| hour | `0`–`23` |\n| day of month | `1`–`31` |\n| month | `1`–`12` or `JAN`–`DEC` |\n| day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday |\n\nMonth and weekday names are ASCII and case-insensitive. Each field accepts\n`*`, a single value, an inclusive range (`1-5`), a comma-separated list of\nvalues or ranges (`1,3,8-10`), or a step applied to `*` or a range (for\nexample, */15 or `1-30/2`). A step MUST be a positive integer. AHP does\nnot support seconds, years, macros such as `@daily`, or Quartz extensions\nsuch as `?`, `L`, `W`, and `#`.\n\nMinute, hour, and month must all match. When both day-of-month and\nday-of-week are restricted (not `*`), an occurrence matches when either day\nfield matches, following Unix cron semantics.", "properties": { - "kind": { - "const": "cron" - }, "expression": { "type": "string", - "description": "Standard five-field Unix cron expression." + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, "timeZone": { "type": "string", - "description": "IANA time-zone identifier." + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } }, "required": [ - "kind", "expression", "timeZone" ] }, "AutomationScheduleTrigger": { "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "schedule" }, "schedule": { - "$ref": "#/$defs/AutomationSchedule" + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." }, "misfirePolicy": { - "$ref": "#/$defs/AutomationMisfirePolicy" + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } }, "required": [ @@ -5043,29 +4976,30 @@ }, "AutomationEventTrigger": { "type": "object", + "description": "Starts runs from events understood by the owning host.\n\nEvent trigger types, event ids, and configuration are discovered through\n`listAutomationTriggerDefinitions`. A client that does not understand a\nhost-defined trigger can still preserve and display it without interpreting\nits configuration.", "properties": { "id": { "type": "string", - "description": "Stable within the automation definition." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, "kind": { "const": "event" }, "type": { "type": "string", - "description": "Stable host-defined trigger type." + "description": "Matches {@link AutomationTriggerDefinition.type}." }, "events": { "type": "array", "items": { "type": "string" }, - "description": "Selected event actions." + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Schema-defined values. Unknown entries must survive round-trips." + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } }, "required": [ @@ -5077,15 +5011,19 @@ }, "AutomationTriggerEventDefinition": { "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for selection UI." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of when this event fires." } }, "required": [ @@ -5095,24 +5033,30 @@ }, "AutomationTriggerDefinition": { "type": "object", + "description": "Describes one host-defined event trigger type available for a prospective\nautomation session template.\n\nTrigger definitions are discovery metadata, not durable automation state.\nHosts may return different definitions for different providers, working\ndirectories, or session configuration.", "properties": { "type": { - "type": "string" + "type": "string", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, "title": { - "type": "string" + "type": "string", + "description": "Human-readable trigger type name." }, "description": { - "type": "string" + "type": "string", + "description": "Optional longer explanation of the trigger source." }, "events": { "type": "array", "items": { "$ref": "#/$defs/AutomationTriggerEventDefinition" - } + }, + "description": "Events clients may select for this trigger type." }, "configSchema": { - "$ref": "#/$defs/ConfigSchema" + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } }, "required": [ @@ -5123,57 +5067,65 @@ }, "AutomationSessionTemplate": { "type": "object", + "description": "Template from which the host creates a fresh session for each automation run.\n\nThe host revalidates every selection when the run starts. Definitions never\ncarry credentials, confirmation decisions, or durable permission grants.", "properties": { "provider": { - "type": "string" + "type": "string", + "description": "Provider id. Omit to use the host's default provider." }, "model": { - "$ref": "#/$defs/ModelSelection" + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." }, "agent": { - "$ref": "#/$defs/AgentSelection" + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." }, "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" }, - "description": "Absence means a workspace-less session." + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Values resolved through `resolveSessionConfig`." + "description": "Session configuration values accepted by `createSession`, normally\nobtained from `resolveSessionConfig`." } } }, "AutomationDefinition": { "type": "object", + "description": "Durable, client-editable definition of an automation.\n\nA definition combines the initial user message, the session template used\nfor each run, and zero or more automatic triggers. Runtime state, run\nhistory, revisions, timestamps, and currently allowed operations live on\n{@link AutomationState} rather than in the definition.", "properties": { "title": { - "type": "string" + "type": "string", + "description": "Human-readable automation name." }, "message": { "$ref": "#/$defs/Message", - "description": "Initial user message sent to each new session." + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, "session": { - "$ref": "#/$defs/AutomationSessionTemplate" + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, "enabled": { "type": "boolean", - "description": "Controls automatic triggers; manual runs remain permitted." + "description": "Whether automatic triggers may create runs. Manual runs remain available\nwhenever {@link AutomationOperation.Run} is advertised." }, "triggers": { "type": "array", "items": { "$ref": "#/$defs/AutomationTrigger" }, - "description": "Empty means manual-only." + "description": "Automatic triggers. An empty list means manual-only." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." } }, "required": [ @@ -5186,58 +5138,73 @@ }, "AutomationRuntimeState": { "type": "object", + "description": "Host-resolved execution context that is useful to clients but is not part of\nthe editable definition.", "properties": { "workingDirectories": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Effective working directories after host-side preparation, such as\nmaterializing a managed workspace." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined runtime metadata." } } }, "AutomationSummary": { "type": "object", + "description": "Lightweight root-catalogue projection of an automation.\n\nReturned by `listAutomations` and carried by root automation notifications,\nthis contains enough information to render a list without subscribing to\nevery `ahp-automation:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation:` URI." }, "title": { - "type": "string" + "type": "string", + "description": "Current {@link AutomationDefinition.title}." }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." }, "triggerCount": { - "type": "number" + "type": "number", + "description": "Number of automatic triggers in the current definition." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "lastRun": { - "$ref": "#/$defs/AutomationRunSummary" + "$ref": "#/$defs/AutomationRunSummary", + "description": "Most recent retained run, when any run exists." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonic definition revision used for optimistic concurrency." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." } }, "required": [ @@ -5253,47 +5220,58 @@ }, "AutomationState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation:` resource.\n\nThe host owns definition revisions, trigger evaluation, run claims, run\nretention, and operation availability. Clients render this state and submit\ncommands; they never run a fallback scheduler for a host-owned definition.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation channel." }, "definition": { - "$ref": "#/$defs/AutomationDefinition" + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." }, "revision": { - "type": "number" + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." }, "nextRunAt": { - "type": "string" + "type": "string", + "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." }, "runs": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunSummary" }, - "description": "Newest-first retained run summaries." + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." }, "runsNextCursor": { - "type": "string" + "type": "string", + "description": "Opaque cursor for the next older run-history page." }, "runtime": { - "$ref": "#/$defs/AutomationRuntimeState" + "$ref": "#/$defs/AutomationRuntimeState", + "description": "Optional host-resolved execution context." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationOperation" - } + }, + "description": "Operations currently permitted for this automation." }, "createdAt": { - "type": "string" + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, "modifiedAt": { - "type": "string" + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." } }, "required": [ @@ -5308,9 +5286,11 @@ }, "AutomationRunBlocker": { "type": "object", + "description": "Summary of why a run cannot currently make progress.", "properties": { "kind": { - "$ref": "#/$defs/AutomationRunBlockerKind" + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." } }, "required": [ @@ -5319,6 +5299,7 @@ }, "AutomationManualRunCause": { "type": "object", + "description": "Cause recorded for a client-requested manual run.", "properties": { "kind": { "const": "manual" @@ -5330,23 +5311,27 @@ }, "AutomationTriggeredRunCause": { "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", "properties": { "kind": { "const": "trigger" }, "triggerId": { - "type": "string" + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." }, "scheduledFor": { - "type": "string" + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." }, "catchUp": { - "type": "boolean" + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." }, "event": { "type": "object", "additionalProperties": {}, - "description": "Host-defined event provenance containing no secrets." + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." } }, "required": [ @@ -5356,12 +5341,14 @@ }, "AutomationPendingRunLifecycle": { "type": "object", + "description": "A durable run exists but has not begun external execution.", "properties": { "status": { "const": "pending" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." } }, "required": [ @@ -5371,15 +5358,18 @@ }, "AutomationRunningRunLifecycle": { "type": "object", + "description": "The run is actively executing linked sessions.", "properties": { "status": { "const": "running" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." } }, "required": [ @@ -5390,18 +5380,22 @@ }, "AutomationBlockedRunLifecycle": { "type": "object", + "description": "The run started but is temporarily unable to progress.", "properties": { "status": { "const": "blocked" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "blocker": { - "$ref": "#/$defs/AutomationRunBlocker" + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." } }, "required": [ @@ -5413,21 +5407,26 @@ }, "AutomationCompletedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", "properties": { "status": { "const": "completed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Completion timestamp in ISO 8601 format." }, "usage": { - "$ref": "#/$defs/UsageInfo" + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." } }, "required": [ @@ -5439,21 +5438,26 @@ }, "AutomationFailedRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a run that ended with an error.\n\n`startedAt` is absent when failure occurred before execution began, such as\nsession-template validation or workspace preparation.", "properties": { "status": { "const": "failed" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Failure timestamp in ISO 8601 format." }, "error": { - "$ref": "#/$defs/ErrorInfo" + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." } }, "required": [ @@ -5465,18 +5469,22 @@ }, "AutomationCancelledRunLifecycle": { "type": "object", + "description": "Terminal lifecycle for a cancelled run.\n\n`startedAt` is absent when cancellation completed while the run was still\npending.", "properties": { "status": { "const": "cancelled" }, "createdAt": { - "type": "string" + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, "startedAt": { - "type": "string" + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, "completedAt": { - "type": "string" + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." } }, "required": [ @@ -5487,6 +5495,7 @@ }, "AutomationRunArtifact": { "type": "object", + "description": "Fetchable output produced at run scope rather than by one specific session.\n\nThe inherited {@link ContentRef} identifies how the client obtains the\ncontent. Session-specific edits, transcripts, and tool results remain on\ntheir session and chat channels.", "properties": { "uri": { "$ref": "#/$defs/URI", @@ -5505,14 +5514,17 @@ "description": "Content nonce" }, "id": { - "type": "string" + "type": "string", + "description": "Stable artifact id within this run, used by artifact actions." }, "label": { - "type": "string" + "type": "string", + "description": "Human-readable label suitable for run-history UI." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." } }, "required": [ @@ -5523,37 +5535,47 @@ }, "AutomationRunSummary": { "type": "object", + "description": "Lightweight projection of a run retained in its automation's history.\n\nA summary contains enough information to render run history without\nsubscribing to every `ahp-automation-run:` resource.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "sessionCount": { - "type": "number" + "type": "number", + "description": "Number of linked sessions, including attempts and workers." }, "artifactCount": { - "type": "number" + "type": "number", + "description": "Number of run-scoped artifacts, when cheaply available." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined summary metadata." } }, "required": [ @@ -5567,43 +5589,53 @@ }, "AutomationRunState": { "type": "object", + "description": "Authoritative state of one subscribed `ahp-automation-run:` resource.\n\nThe run channel owns task-level lifecycle, provenance, linked-session\nmembership, artifacts, and cancellation availability. Linked session and\nchat channels remain authoritative for transcripts, tools, confirmations,\nchangesets, and per-session lifecycle.", "properties": { "resource": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "URI of this automation-run channel." }, "automation": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." }, "cause": { - "$ref": "#/$defs/AutomationRunCause" + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." }, "lifecycle": { - "$ref": "#/$defs/AutomationRunLifecycle" + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle." }, "sessions": { "type": "array", "items": { "$ref": "#/$defs/URI" - } + }, + "description": "Ordered, unique session URIs belonging to this run. Entries may represent\nretries, parallel workers, or delegated attempts." }, "primarySession": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." }, "artifacts": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunArtifact" - } + }, + "description": "Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}." }, "operations": { "type": "array", "items": { "$ref": "#/$defs/AutomationRunOperation" - } + }, + "description": "Operations currently permitted for this run." }, "_meta": { "type": "object", - "additionalProperties": {} + "additionalProperties": {}, + "description": "Opaque host-defined run metadata." } }, "required": [ @@ -5653,7 +5685,8 @@ "description": "A primitive JSON value: a string, number, boolean, or `null`." }, "SessionOrigin": { - "$ref": "#/$defs/AutomationSessionOrigin" + "$ref": "#/$defs/AutomationSessionOrigin", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." }, "SessionInputRequest": { "oneOf": [ @@ -6050,22 +6083,6 @@ ], "description": "A content part within terminal output." }, - "AutomationSchedule": { - "oneOf": [ - { - "$ref": "#/$defs/AutomationHourlySchedule" - }, - { - "$ref": "#/$defs/AutomationDailySchedule" - }, - { - "$ref": "#/$defs/AutomationWeeklySchedule" - }, - { - "$ref": "#/$defs/AutomationCronSchedule" - } - ] - }, "AutomationTrigger": { "oneOf": [ { @@ -6074,7 +6091,8 @@ { "$ref": "#/$defs/AutomationEventTrigger" } - ] + ], + "description": "An automatic cause that can create runs for an enabled automation.\n\nManual execution is not represented as a trigger. An empty trigger list\ntherefore means the automation is manual-only." }, "AutomationRunCause": { "oneOf": [ @@ -6084,7 +6102,8 @@ { "$ref": "#/$defs/AutomationTriggeredRunCause" } - ] + ], + "description": "Immutable provenance describing why a run was created." }, "AutomationRunLifecycle": { "oneOf": [ @@ -6106,7 +6125,8 @@ { "$ref": "#/$defs/AutomationCancelledRunLifecycle" } - ] + ], + "description": "Discriminated lifecycle of an automation run." }, "URI": { "type": "string", @@ -6258,24 +6278,13 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, - "AutomationWeekday": { - "enum": [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday" - ], - "type": "string" - }, "AutomationMisfirePolicy": { "enum": [ "skip", "runOnce" ], - "type": "string" + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." }, "AutomationOperation": { "enum": [ @@ -6283,7 +6292,8 @@ "dispose", "run" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for an automation.\n\nThe list on {@link AutomationState.operations} is authoritative and may\nchange over time. Clients MUST NOT infer permission from capabilities alone:\ncapabilities describe what the host implementation can support, while\noperations describe what is allowed for this particular automation now." }, "AutomationRunBlockerKind": { "enum": [ @@ -6292,13 +6302,15 @@ "authentication", "clientExecution" ], - "type": "string" + "type": "string", + "description": "Coarse reason a run is blocked.\n\nDetailed prompts, confirmations, authentication requests, and tool state\nremain authoritative on linked session and chat channels." }, "AutomationRunOperation": { "enum": [ "cancel" ], - "type": "string" + "type": "string", + "description": "Operations the host currently permits for a run." } } } diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 1e0b143d0..393ced899 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -715,8 +715,7 @@ const STATE_ENUMS = [ 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', - 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', - 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', 'AutomationRunOperation', ]; @@ -851,11 +850,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, { name: 'AutomationSessionOrigin' }, - { name: 'AutomationLocalTime' }, - { name: 'AutomationHourlySchedule' }, - { name: 'AutomationDailySchedule' }, - { name: 'AutomationWeeklySchedule' }, - { name: 'AutomationCronSchedule' }, + { name: 'AutomationSchedule' }, { name: 'AutomationScheduleTrigger' }, { name: 'AutomationEventTrigger' }, { name: 'AutomationTriggerEventDefinition' }, @@ -1112,19 +1107,6 @@ const SESSION_ORIGIN_UNION: UnionConfig = { injectDiscriminantOnMarshal: true, }; -const AUTOMATION_SCHEDULE_UNION: UnionConfig = { - name: 'AutomationSchedule', - discriminantField: 'kind', - doc: 'AutomationSchedule is the calendar schedule for an automation trigger.', - variants: [ - { variantName: 'Hourly', innerType: 'AutomationHourlySchedule', wireValue: 'hourly' }, - { variantName: 'Daily', innerType: 'AutomationDailySchedule', wireValue: 'daily' }, - { variantName: 'Weekly', innerType: 'AutomationWeeklySchedule', wireValue: 'weekly' }, - { variantName: 'Cron', innerType: 'AutomationCronSchedule', wireValue: 'cron' }, - ], - injectDiscriminantOnMarshal: true, -}; - const AUTOMATION_TRIGGER_UNION: UnionConfig = { name: 'AutomationTrigger', discriminantField: 'kind', @@ -1483,8 +1465,6 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); - lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); @@ -1709,7 +1689,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'InitializeParams' }, { name: 'InitializeResult' }, { name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' }, { name: 'AutomationExecutionCapabilities' }, { name: 'AutomationCreateCapability' }, - { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationCronScheduleCapability' }, + { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, { name: 'AutomationSchedulePreviewCapability' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, @@ -1743,7 +1723,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'ChangesetOperationFollowUp' }, { name: 'ListAutomationsParams' }, { name: 'ListAutomationsResult' }, { name: 'ListAutomationTriggerDefinitionsParams' }, { name: 'ListAutomationTriggerDefinitionsResult' }, - { name: 'CreateAutomationParams' }, { name: 'AutomationDefinitionPatch' }, + { name: 'CreateAutomationParams' }, { name: 'AutomationImportIdentity' }, { name: 'AutomationDefinitionPatch' }, { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, @@ -2250,7 +2230,6 @@ function checkExhaustiveness(project: Project): void { 'ToolCallConfirmationState', 'ReconnectResult', 'SessionOrigin', - 'AutomationSchedule', 'AutomationTrigger', 'AutomationRunCause', 'AutomationRunLifecycle', diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index d3ee7150f..5ede4cd1b 100644 --- a/scripts/generate-json-schema.test.ts +++ b/scripts/generate-json-schema.test.ts @@ -173,18 +173,14 @@ describe('generated JSON schemas', () => { ); }); - it('preserves nested automation capability objects', () => { + it('preserves automation schedule restrictions', () => { if (file !== 'commands.schema.json') { return; } const defs = schema.$defs as Record>; const schedules = defs.AutomationScheduleCapabilities; const properties = schedules.properties as Record>; - assert.deepEqual(properties.cron.$ref, '#/$defs/AutomationCronScheduleCapability'); - const cron = defs.AutomationCronScheduleCapability; - const cronProperties = cron.properties as Record>; - assert.deepEqual(cronProperties.dialect.enum, ['unix5']); - assert.equal(cronProperties.minIntervalMinutes.type, 'number'); + assert.equal(properties.minIntervalMinutes.type, 'number'); }); it('constrains every ChatOrigin branch to a distinct kind', () => { diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 251e1e21e..afc226943 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -931,8 +931,7 @@ const STATE_ENUMS = [ 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', - 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', - 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', 'AutomationRunOperation', ]; @@ -990,9 +989,7 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', - 'AutomationSessionOrigin', - 'AutomationLocalTime', 'AutomationHourlySchedule', 'AutomationDailySchedule', - 'AutomationWeeklySchedule', 'AutomationCronSchedule', + 'AutomationSessionOrigin', 'AutomationSchedule', 'AutomationScheduleTrigger', 'AutomationEventTrigger', 'AutomationTriggerEventDefinition', 'AutomationTriggerDefinition', 'AutomationSessionTemplate', 'AutomationDefinition', 'AutomationRuntimeState', @@ -1274,18 +1271,6 @@ const SESSION_ORIGIN_UNION: UnionConfig = { injectDiscriminantOnSerialize: true, }; -const AUTOMATION_SCHEDULE_UNION: UnionConfig = { - name: 'AutomationSchedule', - discriminantField: 'kind', - variants: [ - { caseName: 'Hourly', structName: 'AutomationHourlySchedule', discriminantValue: 'hourly' }, - { caseName: 'Daily', structName: 'AutomationDailySchedule', discriminantValue: 'daily' }, - { caseName: 'Weekly', structName: 'AutomationWeeklySchedule', discriminantValue: 'weekly' }, - { caseName: 'Cron', structName: 'AutomationCronSchedule', discriminantValue: 'cron' }, - ], - injectDiscriminantOnSerialize: true, -}; - const AUTOMATION_TRIGGER_UNION: UnionConfig = { name: 'AutomationTrigger', discriminantField: 'kind', @@ -1398,8 +1383,6 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); - lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); @@ -1675,7 +1658,7 @@ const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'AutomationCapabilities', 'AutomationExecutionCapabilities', 'AutomationCreateCapability', - 'AutomationScheduleCapabilities', 'AutomationCronScheduleCapability', + 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', 'AutomationSchedulePreviewCapability', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', @@ -1706,7 +1689,7 @@ const COMMAND_STRUCTS = [ 'ChangesetOperationFollowUp', 'ListAutomationsParams', 'ListAutomationsResult', 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', - 'CreateAutomationParams', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'CreateAutomationParams', 'AutomationImportIdentity', 'AutomationDefinitionPatch', 'UpdateAutomationParams', 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', @@ -2251,7 +2234,6 @@ function checkExhaustiveness(project: Project): void { 'JsonRpcErrorCode', // type-level alias over JsonRpcErrorCodes const enum 'ReconnectResult', // RECONNECT_RESULT_UNION discriminated union 'SessionOrigin', // SESSION_ORIGIN_UNION discriminated union - 'AutomationSchedule', // AUTOMATION_SCHEDULE_UNION discriminated union 'AutomationTrigger', // AUTOMATION_TRIGGER_UNION discriminated union 'AutomationRunCause', // AUTOMATION_RUN_CAUSE_UNION discriminated union 'AutomationRunLifecycle', // AUTOMATION_RUN_LIFECYCLE_UNION discriminated union diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 098471206..ec7df8561 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -662,8 +662,7 @@ const STATE_ENUMS = [ 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', - 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', - 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', 'AutomationRunOperation', ]; @@ -819,11 +818,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, { name: 'AutomationSessionOrigin', omitDiscriminants: true }, - { name: 'AutomationLocalTime' }, - { name: 'AutomationHourlySchedule', omitDiscriminants: true }, - { name: 'AutomationDailySchedule', omitDiscriminants: true }, - { name: 'AutomationWeeklySchedule', omitDiscriminants: true }, - { name: 'AutomationCronSchedule', omitDiscriminants: true }, + { name: 'AutomationSchedule' }, { name: 'AutomationScheduleTrigger', omitDiscriminants: true }, { name: 'AutomationEventTrigger', omitDiscriminants: true }, { name: 'AutomationTriggerEventDefinition' }, @@ -1084,18 +1079,6 @@ const SESSION_ORIGIN_UNION: UnionConfig = { ], }; -const AUTOMATION_SCHEDULE_UNION: UnionConfig = { - name: 'AutomationSchedule', - discriminantField: 'kind', - doc: 'Calendar schedule for an automation trigger.', - variants: [ - { variantName: 'Hourly', innerType: 'AutomationHourlySchedule', wireValue: 'hourly' }, - { variantName: 'Daily', innerType: 'AutomationDailySchedule', wireValue: 'daily' }, - { variantName: 'Weekly', innerType: 'AutomationWeeklySchedule', wireValue: 'weekly' }, - { variantName: 'Cron', innerType: 'AutomationCronSchedule', wireValue: 'cron' }, - ], -}; - const AUTOMATION_TRIGGER_UNION: UnionConfig = { name: 'AutomationTrigger', discriminantField: 'kind', @@ -1282,8 +1265,6 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); - lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); @@ -1545,7 +1526,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'InitializeParams' }, { name: 'InitializeResult' }, { name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' }, { name: 'AutomationExecutionCapabilities' }, { name: 'AutomationCreateCapability' }, - { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationCronScheduleCapability' }, + { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, { name: 'AutomationSchedulePreviewCapability' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, @@ -1580,7 +1561,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'ChangesetOperationFollowUp' }, { name: 'ListAutomationsParams' }, { name: 'ListAutomationsResult' }, { name: 'ListAutomationTriggerDefinitionsParams' }, { name: 'ListAutomationTriggerDefinitionsResult' }, - { name: 'CreateAutomationParams' }, { name: 'AutomationDefinitionPatch' }, + { name: 'CreateAutomationParams' }, { name: 'AutomationImportIdentity' }, { name: 'AutomationDefinitionPatch' }, { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, @@ -1612,7 +1593,7 @@ function generateCommandsFile(project: Project): string { lines.push('#[allow(unused_imports)]'); lines.push('use crate::actions::{ActionEnvelope, StateAction};'); lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationExecutionLifetime, AutomationSchedule, AutomationScheduleKind, AutomationSessionTemplate, AutomationSummary, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); + lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationExecutionLifetime, AutomationSchedule, AutomationSessionTemplate, AutomationSummary, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); @@ -2043,7 +2024,6 @@ function checkExhaustiveness(project: Project): void { 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ReconnectResult', 'SessionOrigin', - 'AutomationSchedule', 'AutomationTrigger', 'AutomationRunCause', 'AutomationRunLifecycle', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 406ad9fc7..6b731b1a5 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -622,8 +622,7 @@ const STATE_ENUMS = [ 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', 'SessionOriginKind', - 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationScheduleKind', - 'AutomationWeekday', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', 'AutomationRunOperation', ]; @@ -681,9 +680,7 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', - 'AutomationSessionOrigin', - 'AutomationLocalTime', 'AutomationHourlySchedule', 'AutomationDailySchedule', - 'AutomationWeeklySchedule', 'AutomationCronSchedule', + 'AutomationSessionOrigin', 'AutomationSchedule', 'AutomationScheduleTrigger', 'AutomationEventTrigger', 'AutomationTriggerEventDefinition', 'AutomationTriggerDefinition', 'AutomationSessionTemplate', 'AutomationDefinition', 'AutomationRuntimeState', @@ -1164,18 +1161,6 @@ const SESSION_ORIGIN_UNION: UnionConfig = { injectDiscriminantOnEncode: true, }; -const AUTOMATION_SCHEDULE_UNION: UnionConfig = { - name: 'AutomationSchedule', - discriminantField: 'kind', - variants: [ - { caseName: 'hourly', structName: 'AutomationHourlySchedule', discriminantValue: 'hourly' }, - { caseName: 'daily', structName: 'AutomationDailySchedule', discriminantValue: 'daily' }, - { caseName: 'weekly', structName: 'AutomationWeeklySchedule', discriminantValue: 'weekly' }, - { caseName: 'cron', structName: 'AutomationCronSchedule', discriminantValue: 'cron' }, - ], - injectDiscriminantOnEncode: true, -}; - const AUTOMATION_TRIGGER_UNION: UnionConfig = { name: 'AutomationTrigger', discriminantField: 'kind', @@ -1281,8 +1266,6 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_ORIGIN_UNION)); lines.push(''); - lines.push(generateDiscriminatedUnion(AUTOMATION_SCHEDULE_UNION)); - lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_TRIGGER_UNION)); lines.push(''); lines.push(generateDiscriminatedUnion(AUTOMATION_RUN_CAUSE_UNION)); @@ -1568,7 +1551,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'AutomationCapabilities', 'AutomationExecutionCapabilities', 'AutomationCreateCapability', - 'AutomationScheduleCapabilities', 'AutomationCronScheduleCapability', + 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', 'AutomationSchedulePreviewCapability', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', @@ -1599,7 +1582,7 @@ const COMMAND_STRUCTS = [ 'ChangesetOperationFollowUp', 'ListAutomationsParams', 'ListAutomationsResult', 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', - 'CreateAutomationParams', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'CreateAutomationParams', 'AutomationImportIdentity', 'AutomationDefinitionPatch', 'UpdateAutomationParams', 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', @@ -2259,7 +2242,6 @@ function checkExhaustiveness(project: Project): void { 'JsonRpcErrorCode', // type-level alias over JsonRpcErrorCodes const enum 'ReconnectResult', // RECONNECT_RESULT_UNION discriminated union 'SessionOrigin', // SESSION_ORIGIN_UNION discriminated union - 'AutomationSchedule', // AUTOMATION_SCHEDULE_UNION discriminated union 'AutomationTrigger', // AUTOMATION_TRIGGER_UNION discriminated union 'AutomationRunCause', // AUTOMATION_RUN_CAUSE_UNION discriminated union 'AutomationRunLifecycle', // AUTOMATION_RUN_LIFECYCLE_UNION discriminated union diff --git a/types/channels-automation-run/actions.ts b/types/channels-automation-run/actions.ts index 7614919cd..6d668f0e3 100644 --- a/types/channels-automation-run/actions.ts +++ b/types/channels-automation-run/actions.ts @@ -1,5 +1,6 @@ /** - * Automation Run Channel Actions. + * Automation Run Channel Actions — mutations and side-effect requests scoped + * to an `ahp-automation-run:` channel. * * @module channels-automation-run/actions */ @@ -8,45 +9,102 @@ import { ActionType } from '../common/actions.js'; import type { URI } from '../common/state.js'; import type { AutomationRunArtifact, AutomationRunLifecycle, AutomationRunOperation } from './state.js'; -/** @category Automation Run Actions */ +/** + * Replace the run lifecycle and currently allowed operations atomically. + * + * The host dispatches this action for every lifecycle transition. Terminal + * lifecycles normally carry an empty operations list. + * + * @category Automation Run Actions + * @version 1 + */ export interface AutomationRunLifecycleChangedAction { type: ActionType.AutomationRunLifecycleChanged; + /** Complete replacement lifecycle. */ lifecycle: AutomationRunLifecycle; + /** Complete replacement operation list. */ operations: AutomationRunOperation[]; } -/** @category Automation Run Actions */ +/** + * Add a session to the run's ordered session catalogue. + * + * Session URIs are unique. Setting an existing URI is a no-op. + * + * @category Automation Run Actions + * @version 1 + */ export interface AutomationRunSessionSetAction { type: ActionType.AutomationRunSessionSet; + /** Session URI to append when it is not already linked. */ session: URI; } -/** @category Automation Run Actions */ +/** + * Remove a linked session from the run. + * + * Removing the current primary session also clears + * {@link AutomationRunState.primarySession}. An unknown URI is a no-op. + * + * @category Automation Run Actions + * @version 1 + */ export interface AutomationRunSessionRemovedAction { type: ActionType.AutomationRunSessionRemoved; + /** Linked session URI to remove. */ session: URI; } -/** @category Automation Run Actions */ +/** + * Select or clear the session clients should open first for this run. + * + * @category Automation Run Actions + * @version 1 + */ export interface AutomationRunPrimarySessionChangedAction { type: ActionType.AutomationRunPrimarySessionChanged; + /** New primary linked session, or omitted to clear the selection. */ primarySession?: URI; } -/** @category Automation Run Actions */ +/** + * Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}. + * + * @category Automation Run Actions + * @version 1 + */ export interface AutomationRunArtifactSetAction { type: ActionType.AutomationRunArtifactSet; + /** New or replacement artifact. */ artifact: AutomationRunArtifact; } -/** @category Automation Run Actions */ +/** + * Remove a run-scoped artifact by id. + * + * The action is a no-op when the id is not present. + * + * @category Automation Run Actions + * @version 1 + */ export interface AutomationRunArtifactRemovedAction { type: ActionType.AutomationRunArtifactRemoved; + /** {@link AutomationRunArtifact.id} to remove. */ artifactId: string; } /** + * Ask the host to cancel this run. + * + * This is the only client-dispatchable automation-run action. It is a + * side-effect request and deliberately leaves optimistic state unchanged. The + * authoritative outcome arrives later through + * {@link AutomationRunLifecycleChangedAction}: cancellation may transition to + * `cancelled`, or the run may complete or fail before cancellation takes + * effect. + * * @category Automation Run Actions + * @version 1 * @clientDispatchable */ export interface AutomationRunCancelRequestedAction { diff --git a/types/channels-automation-run/state.ts b/types/channels-automation-run/state.ts index 77a264379..43d14488b 100644 --- a/types/channels-automation-run/state.ts +++ b/types/channels-automation-run/state.ts @@ -6,103 +6,214 @@ import type { ContentRef, ErrorInfo, URI, UsageInfo } from '../common/state.js'; -/** @category Automation Run State */ +/** + * Lifecycle status of one automation run. + * + * `completed`, `failed`, and `cancelled` are terminal. `blocked` is + * non-terminal: the host may return the run to `running` after the linked + * session resolves the blocker. + * + * @category Automation Run State + */ export const enum AutomationRunStatus { + /** The durable run record exists but execution has not started. */ Pending = 'pending', + /** One or more linked sessions are actively executing. */ Running = 'running', + /** Execution is paused on an interaction or client-side dependency. */ Blocked = 'blocked', + /** Execution finished successfully. */ Completed = 'completed', + /** Execution ended with an error. */ Failed = 'failed', + /** Execution ended because cancellation was accepted. */ Cancelled = 'cancelled', } -/** @category Automation Run State */ +/** + * Coarse reason a run is blocked. + * + * Detailed prompts, confirmations, authentication requests, and tool state + * remain authoritative on linked session and chat channels. + * + * @category Automation Run State + */ export const enum AutomationRunBlockerKind { + /** A linked session is waiting for an answer to a user-input request. */ UserInput = 'userInput', + /** A linked session is waiting for tool confirmation. */ ToolConfirmation = 'toolConfirmation', + /** Execution requires authentication or renewed credentials. */ Authentication = 'authentication', + /** Work must be performed by or delegated to a connected client. */ ClientExecution = 'clientExecution', } -/** @category Automation Run State */ +/** + * Summary of why a run cannot currently make progress. + * + * @category Automation Run State + */ export interface AutomationRunBlocker { + /** Category of the outstanding dependency. */ kind: AutomationRunBlockerKind; } -/** @category Automation Run State */ +/** + * Discriminant describing what created an automation run. + * + * @category Automation Run State + */ export const enum AutomationRunCauseKind { + /** A client explicitly invoked `runAutomation`. */ Manual = 'manual', + /** An automatic schedule or event trigger fired. */ Trigger = 'trigger', } -/** @category Automation Run State */ +/** + * Cause recorded for a client-requested manual run. + * + * @category Automation Run State + */ export interface AutomationManualRunCause { kind: AutomationRunCauseKind.Manual; } -/** @category Automation Run State */ +/** + * Cause recorded for a run created by one of the automation's triggers. + * + * @category Automation Run State + */ export interface AutomationTriggeredRunCause { kind: AutomationRunCauseKind.Trigger; + /** Matches the stable {@link AutomationTrigger.id} in the definition. */ triggerId: string; + /** + * Intended schedule occurrence as an ISO 8601 timestamp. Present for + * schedule triggers and normally absent for event triggers. + */ scheduledFor?: string; + /** + * `true` when this is a catch-up run created by + * {@link AutomationMisfirePolicy.RunOnce}. + */ catchUp?: boolean; - /** Host-defined event provenance containing no secrets. */ + /** + * Host-defined, non-secret event provenance suitable for display or audit. + * This is descriptive context, not an input that clients replay. + */ event?: Record; } -/** @category Automation Run State */ +/** + * Immutable provenance describing why a run was created. + * + * @category Automation Run State + */ export type AutomationRunCause = | AutomationManualRunCause | AutomationTriggeredRunCause; -/** @category Automation Run State */ +/** + * A durable run exists but has not begun external execution. + * + * @category Automation Run State + */ export interface AutomationPendingRunLifecycle { status: AutomationRunStatus.Pending; + /** Run creation timestamp in ISO 8601 format. */ createdAt: string; } -/** @category Automation Run State */ +/** + * The run is actively executing linked sessions. + * + * @category Automation Run State + */ export interface AutomationRunningRunLifecycle { status: AutomationRunStatus.Running; + /** Run creation timestamp in ISO 8601 format. */ createdAt: string; + /** First execution start timestamp in ISO 8601 format. */ startedAt: string; } -/** @category Automation Run State */ +/** + * The run started but is temporarily unable to progress. + * + * @category Automation Run State + */ export interface AutomationBlockedRunLifecycle { status: AutomationRunStatus.Blocked; + /** Run creation timestamp in ISO 8601 format. */ createdAt: string; + /** First execution start timestamp in ISO 8601 format. */ startedAt: string; + /** Coarse blocker summary; linked sessions contain interaction details. */ blocker: AutomationRunBlocker; } -/** @category Automation Run State */ +/** + * Terminal lifecycle for a successfully completed run. + * + * @category Automation Run State + */ export interface AutomationCompletedRunLifecycle { status: AutomationRunStatus.Completed; + /** Run creation timestamp in ISO 8601 format. */ createdAt: string; + /** First execution start timestamp in ISO 8601 format. */ startedAt: string; + /** Completion timestamp in ISO 8601 format. */ completedAt: string; + /** Optional aggregate model usage across all linked sessions. */ usage?: UsageInfo; } -/** @category Automation Run State */ +/** + * Terminal lifecycle for a run that ended with an error. + * + * `startedAt` is absent when failure occurred before execution began, such as + * session-template validation or workspace preparation. + * + * @category Automation Run State + */ export interface AutomationFailedRunLifecycle { status: AutomationRunStatus.Failed; + /** Run creation timestamp in ISO 8601 format. */ createdAt: string; + /** First execution start timestamp in ISO 8601 format, when execution began. */ startedAt?: string; + /** Failure timestamp in ISO 8601 format. */ completedAt: string; + /** Stable machine-readable and human-readable failure information. */ error: ErrorInfo; } -/** @category Automation Run State */ +/** + * Terminal lifecycle for a cancelled run. + * + * `startedAt` is absent when cancellation completed while the run was still + * pending. + * + * @category Automation Run State + */ export interface AutomationCancelledRunLifecycle { status: AutomationRunStatus.Cancelled; + /** Run creation timestamp in ISO 8601 format. */ createdAt: string; + /** First execution start timestamp in ISO 8601 format, when execution began. */ startedAt?: string; + /** Cancellation completion timestamp in ISO 8601 format. */ completedAt: string; } -/** @category Automation Run State */ +/** + * Discriminated lifecycle of an automation run. + * + * @category Automation Run State + */ export type AutomationRunLifecycle = | AutomationPendingRunLifecycle | AutomationRunningRunLifecycle @@ -111,40 +222,93 @@ export type AutomationRunLifecycle = | AutomationFailedRunLifecycle | AutomationCancelledRunLifecycle; -/** @category Automation Run State */ +/** + * Operations the host currently permits for a run. + * + * @category Automation Run State + */ export const enum AutomationRunOperation { + /** Request cancellation with `automationRun/cancelRequested`. */ Cancel = 'cancel', } -/** @category Automation Run State */ +/** + * Fetchable output produced at run scope rather than by one specific session. + * + * The inherited {@link ContentRef} identifies how the client obtains the + * content. Session-specific edits, transcripts, and tool results remain on + * their session and chat channels. + * + * @category Automation Run State + */ export interface AutomationRunArtifact extends ContentRef { + /** Stable artifact id within this run, used by artifact actions. */ id: string; + /** Human-readable label suitable for run-history UI. */ label: string; + /** Opaque host-defined artifact metadata. */ _meta?: Record; } -/** @category Automation Run State */ +/** + * Lightweight projection of a run retained in its automation's history. + * + * A summary contains enough information to render run history without + * subscribing to every `ahp-automation-run:` resource. + * + * @category Automation Run State + */ export interface AutomationRunSummary { + /** Subscribable `ahp-automation-run:` URI. */ resource: URI; + /** Owning `ahp-automation:` URI. */ automation: URI; + /** Immutable reason this run was created. */ cause: AutomationRunCause; + /** Current or terminal lifecycle snapshot. */ lifecycle: AutomationRunLifecycle; + /** Session the host recommends opening first, when one has been selected. */ primarySession?: URI; + /** Number of linked sessions, including attempts and workers. */ sessionCount: number; + /** Number of run-scoped artifacts, when cheaply available. */ artifactCount?: number; + /** Operations currently permitted for this run. */ operations: AutomationRunOperation[]; + /** Opaque host-defined summary metadata. */ _meta?: Record; } -/** @category Automation Run State */ +/** + * Authoritative state of one subscribed `ahp-automation-run:` resource. + * + * The run channel owns task-level lifecycle, provenance, linked-session + * membership, artifacts, and cancellation availability. Linked session and + * chat channels remain authoritative for transcripts, tools, confirmations, + * changesets, and per-session lifecycle. + * + * @category Automation Run State + */ export interface AutomationRunState { + /** URI of this automation-run channel. */ resource: URI; + /** Owning `ahp-automation:` URI. */ automation: URI; + /** Immutable reason this run was created. */ cause: AutomationRunCause; + /** Current or terminal lifecycle. */ lifecycle: AutomationRunLifecycle; + /** + * Ordered, unique session URIs belonging to this run. Entries may represent + * retries, parallel workers, or delegated attempts. + */ sessions: URI[]; + /** Session the host recommends opening first, when one has been selected. */ primarySession?: URI; + /** Run-scoped artifacts keyed by {@link AutomationRunArtifact.id}. */ artifacts: AutomationRunArtifact[]; + /** Operations currently permitted for this run. */ operations: AutomationRunOperation[]; + /** Opaque host-defined run metadata. */ _meta?: Record; } diff --git a/types/channels-automation/actions.ts b/types/channels-automation/actions.ts index 7c4c2df19..0315c6d4f 100644 --- a/types/channels-automation/actions.ts +++ b/types/channels-automation/actions.ts @@ -1,5 +1,6 @@ /** - * Automation Channel Actions. + * Automation Channel Actions — server-authored mutations of an + * `ahp-automation:` channel. * * @module channels-automation/actions */ @@ -9,30 +10,75 @@ import type { URI } from '../common/state.js'; import type { AutomationDefinition } from './state.js'; import type { AutomationRunSummary } from '../channels-automation-run/state.js'; -/** @category Automation Actions */ +/** + * Replace the editable definition after a successful `updateAutomation` or + * another host-authorized definition change. + * + * Full replacement semantics apply to `definition`. The reducer also replaces + * the revision and modification timestamp. Omitting `nextRunAt` clears the + * previously projected next occurrence. + * + * @category Automation Actions + * @version 1 + */ export interface AutomationDefinitionChangedAction { type: ActionType.AutomationDefinitionChanged; + /** Complete replacement definition. */ definition: AutomationDefinition; + /** New monotonic revision. */ revision: number; + /** Definition modification timestamp in ISO 8601 format. */ modifiedAt: string; + /** Earliest known future scheduled occurrence, or omitted to clear it. */ nextRunAt?: string; } -/** @category Automation Actions */ +/** + * Upsert one run summary in the retained history. + * + * Existing entries are replaced by {@link AutomationRunSummary.resource}. A + * previously unseen run is inserted at the front because history is + * newest-first. + * + * @category Automation Actions + * @version 1 + */ export interface AutomationRunSummarySetAction { type: ActionType.AutomationRunSummarySet; + /** New or replacement run summary. */ run: AutomationRunSummary; } -/** @category Automation Actions */ +/** + * Remove one retained run summary by its automation-run URI. + * + * The action is a no-op when the URI is not present in the current history + * window. + * + * @category Automation Actions + * @version 1 + */ export interface AutomationRunSummaryRemovedAction { type: ActionType.AutomationRunSummaryRemoved; + /** {@link AutomationRunSummary.resource} to remove. */ run: URI; } -/** @category Automation Actions */ +/** + * Append an older page of run summaries returned by + * `fetchAutomationRuns`. + * + * Entries already present by resource URI are ignored, preserving the + * newest-first ordering of the existing history followed by the fetched page. + * Omitting `nextCursor` marks the end of retained history. + * + * @category Automation Actions + * @version 1 + */ export interface AutomationRunsLoadedAction { type: ActionType.AutomationRunsLoaded; + /** Older run summaries in newest-first order within this page. */ runs: AutomationRunSummary[]; + /** Opaque cursor for the next older page, or omitted at the end. */ nextCursor?: string; } diff --git a/types/channels-automation/commands.ts b/types/channels-automation/commands.ts index f330f3247..2ee8ce7ef 100644 --- a/types/channels-automation/commands.ts +++ b/types/channels-automation/commands.ts @@ -1,5 +1,6 @@ /** - * Automation Channel Commands. + * Automation Commands — catalogue discovery and mutation of + * `ahp-automation:` resources. * * @module channels-automation/commands */ @@ -17,126 +18,259 @@ import type { import type { Message } from '../channels-chat/state.js'; /** + * List the host's automation catalogue without subscribing to every + * automation channel. + * + * Results are lightweight {@link AutomationSummary} entries. Clients SHOULD + * re-run this command after reconnect because root catalogue notifications are + * not replayed. + * * @category Commands * @method listAutomations * @direction Client → Server * @messageType Request + * @version 1 */ export interface ListAutomationsParams extends BaseParams, PaginatedParams { + /** Automation catalogues are listed from the root channel. */ channel: 'ahp-root://'; + /** Optional exact filter on {@link AutomationDefinition.enabled}. */ enabled?: boolean; } -/** @category Commands */ +/** + * One page of the automation catalogue. + * + * @category Commands + */ export interface ListAutomationsResult extends PaginatedResult { + /** Automation summaries in host-defined catalogue order. */ items: AutomationSummary[]; } /** + * Discover event-trigger types available for a prospective session template. + * + * Hosts may vary definitions by provider, workspace, and session + * configuration. Schedule triggers are protocol-defined and therefore do not + * appear in this result. + * * @category Commands * @method listAutomationTriggerDefinitions * @direction Client → Server * @messageType Request + * @version 1 */ export interface ListAutomationTriggerDefinitionsParams extends BaseParams { + /** Trigger definitions are discovered from the root channel. */ channel: 'ahp-root://'; + /** Prospective provider id, or omitted for the host default. */ provider?: string; + /** Prospective ordered working-directory list. */ workingDirectories?: URI[]; + /** Prospective resolved session configuration values. */ sessionConfig?: Record; } -/** @category Commands */ +/** + * Host-defined event trigger types available for the supplied context. + * + * @category Commands + */ export interface ListAutomationTriggerDefinitionsResult { + /** Available event trigger definitions. */ items: AutomationTriggerDefinition[]; } /** + * Stable source identity used to make legacy automation import idempotent. + * + * The host remembers this identity independently of the client-chosen + * automation URI. Retrying an interrupted migration with the same values MUST + * resolve to the previously imported item rather than creating a duplicate. + * + * @category Commands + */ +export interface AutomationImportIdentity { + /** Stable namespace identifying the source implementation or store. */ + source: string; + /** Identifier shared by every item in one import attempt. */ + batchId: string; + /** Stable source-side identifier for this definition within the batch. */ + itemId: string; +} + +/** + * Create a durable automation at a client-chosen URI. + * + * `channel` MUST use the `ahp-automation:` scheme and MUST NOT already identify + * an unrelated automation. The host validates the complete definition, + * persists it, and makes it visible through the root catalogue before + * returning success. + * * @category Commands * @method createAutomation * @direction Client → Server * @messageType Request + * @version 1 */ export interface CreateAutomationParams extends BaseParams { + /** Client-chosen `ahp-automation:` URI for the new definition. */ channel: URI; + /** Complete initial definition. */ definition: AutomationDefinition; - import?: { - source: string; - batchId: string; - itemId: string; - }; + /** Optional idempotency identity when importing a legacy definition. */ + import?: AutomationImportIdentity; } -/** @category Commands */ +/** + * Partial replacement of editable {@link AutomationDefinition} fields. + * + * Omitted fields are unchanged. Supplied arrays and objects replace their + * corresponding values in full; they are not merged recursively. + * + * @category Commands + */ export interface AutomationDefinitionPatch { + /** Replacement human-readable title. */ title?: string; + /** Replacement initial user message. */ message?: Message; + /** Replacement session template. */ session?: AutomationSessionTemplate; + /** Replacement automatic-trigger enabled state. */ enabled?: boolean; + /** Complete replacement trigger list. */ triggers?: AutomationTrigger[]; + /** Complete replacement implementation-defined metadata. */ _meta?: Record; } /** + * Update editable fields of an existing automation using optimistic + * concurrency. + * + * The host accepts the patch only when `expectedRevision` equals the current + * {@link AutomationState.revision}. A stale revision is rejected; clients + * SHOULD reconcile the latest state before retrying. + * * @category Commands * @method updateAutomation * @direction Client → Server * @messageType Request + * @version 1 */ export interface UpdateAutomationParams extends BaseParams { + /** Target `ahp-automation:` URI. */ channel: URI; + /** Revision on which the client based {@link changes}. */ expectedRevision: number; + /** Editable fields to replace. */ changes: AutomationDefinitionPatch; } /** + * Permanently remove an automation. + * + * The target is supplied by {@link BaseParams.channel}. The host rejects the + * command when {@link AutomationOperation.Dispose} is not currently + * advertised, for example while a non-terminal run prevents disposal. + * * @category Commands * @method disposeAutomation * @direction Client → Server * @messageType Request + * @version 1 */ export interface DisposeAutomationParams extends BaseParams {} /** + * Start a manual run of an automation. + * + * Manual execution is independent of {@link AutomationDefinition.enabled}. + * The host persists the run before beginning session side effects. + * * @category Commands * @method runAutomation * @direction Client → Server * @messageType Request + * @version 1 */ export interface RunAutomationParams extends BaseParams { + /** + * Durable client-generated idempotency key. Retrying with the same key and + * automation MUST return the original run URI rather than create another + * run. + */ requestId: string; } -/** @category Commands */ +/** + * Result identifying the existing or newly created run. + * + * @category Commands + */ export interface RunAutomationResult { + /** Subscribable `ahp-automation-run:` URI. */ run: URI; } /** + * Load one older page into the subscribed automation's run-history state. + * + * The response only acknowledges the request. Loaded entries arrive through + * `automation/runsLoaded`, keeping all subscribers synchronized through the + * normal action stream. + * * @category Commands * @method fetchAutomationRuns * @direction Client → Server * @messageType Request + * @version 1 */ export interface FetchAutomationRunsParams extends BaseParams { + /** + * Cursor previously received as {@link AutomationState.runsNextCursor}. + * Omit to request the first page not already included by the snapshot. + */ cursor?: string; } -/** @category Commands */ +/** + * Empty acknowledgement; run summaries are delivered by action. + * + * @category Commands + */ export interface FetchAutomationRunsResult {} /** + * Ask the host to evaluate a schedule without creating an automation. + * + * Clients SHOULD use this command for validation and preview instead of + * implementing their own cron evaluator, especially around time-zone + * transitions. + * * @category Commands * @method previewAutomationSchedule * @direction Client → Server * @messageType Request + * @version 1 */ export interface PreviewAutomationScheduleParams extends BaseParams { + /** Schedule preview is requested from the root channel. */ channel: 'ahp-root://'; + /** Portable AHP cron schedule to evaluate. */ schedule: AutomationSchedule; + /** Requested maximum number of future occurrences; the host MAY cap it. */ count?: number; } -/** @category Commands */ +/** + * Host-canonical future schedule occurrences. + * + * @category Commands + */ export interface PreviewAutomationScheduleResult { + /** Ascending ISO 8601 timestamps. */ items: string[]; } diff --git a/types/channels-automation/state.ts b/types/channels-automation/state.ts index 64c95d42a..8a5810bb9 100644 --- a/types/channels-automation/state.ts +++ b/types/channels-automation/state.ts @@ -10,194 +10,365 @@ import type { AutomationRunSummary } from '../channels-automation-run/state.js'; import type { ModelSelection } from '../channels-root/state.js'; import type { AgentSelection } from '../channels-session/state.js'; -/** @category Automation State */ +/** + * Operations the host currently permits for an automation. + * + * The list on {@link AutomationState.operations} is authoritative and may + * change over time. Clients MUST NOT infer permission from capabilities alone: + * capabilities describe what the host implementation can support, while + * operations describe what is allowed for this particular automation now. + * + * @category Automation State + */ export const enum AutomationOperation { + /** Replace editable fields using `updateAutomation`. */ Update = 'update', + /** Permanently remove the automation using `disposeAutomation`. */ Dispose = 'dispose', + /** Start a manual run using `runAutomation`. */ Run = 'run', } -/** @category Automation State */ +/** + * Availability guarantee for host-owned automatic trigger evaluation. + * + * This describes the authority that owns one automation catalogue. It does not + * prevent a client from connecting to several authorities with different + * lifetimes (for example, one local host and one managed service). + * + * @category Automation State + */ export const enum AutomationExecutionLifetime { + /** + * Automatic triggers are evaluated only while this host process is running. + * Definitions may remain durable across restarts, but occurrences while the + * process is unavailable are handled according to the trigger's + * {@link AutomationMisfirePolicy}. + */ HostLifetime = 'hostLifetime', + /** + * Automatic triggers continue to be evaluated independently of connected + * clients and any particular interactive host process. + */ Managed = 'managed', } -/** @category Automation State */ -export const enum AutomationScheduleKind { - Hourly = 'hourly', - Daily = 'daily', - Weekly = 'weekly', - Cron = 'cron', -} - -/** @category Automation State */ -export const enum AutomationWeekday { - Monday = 'monday', - Tuesday = 'tuesday', - Wednesday = 'wednesday', - Thursday = 'thursday', - Friday = 'friday', - Saturday = 'saturday', - Sunday = 'sunday', -} - -/** @category Automation State */ -export interface AutomationLocalTime { - hour: number; - minute: number; -} - -/** @category Automation State */ -export interface AutomationHourlySchedule { - kind: AutomationScheduleKind.Hourly; -} - -/** @category Automation State */ -export interface AutomationDailySchedule { - kind: AutomationScheduleKind.Daily; - time: AutomationLocalTime; - /** IANA time-zone identifier. */ - timeZone: string; -} - -/** @category Automation State */ -export interface AutomationWeeklySchedule { - kind: AutomationScheduleKind.Weekly; - weekday: AutomationWeekday; - time: AutomationLocalTime; - /** IANA time-zone identifier. */ - timeZone: string; -} - -/** @category Automation State */ -export interface AutomationCronSchedule { - kind: AutomationScheduleKind.Cron; - /** Standard five-field Unix cron expression. */ +/** + * A portable recurring schedule evaluated in a named time zone. + * + * The expression uses exactly five whitespace-separated fields, in this + * order: + * + * | Field | Values | + * | --- | --- | + * | minute | `0`–`59` | + * | hour | `0`–`23` | + * | day of month | `1`–`31` | + * | month | `1`–`12` or `JAN`–`DEC` | + * | day of week | `0`–`7` or `SUN`–`SAT`; both `0` and `7` mean Sunday | + * + * Month and weekday names are ASCII and case-insensitive. Each field accepts + * `*`, a single value, an inclusive range (`1-5`), a comma-separated list of + * values or ranges (`1,3,8-10`), or a step applied to `*` or a range (for + * example, */15 or `1-30/2`). A step MUST be a positive integer. AHP does + * not support seconds, years, macros such as `@daily`, or Quartz extensions + * such as `?`, `L`, `W`, and `#`. + * + * Minute, hour, and month must all match. When both day-of-month and + * day-of-week are restricted (not `*`), an occurrence matches when either day + * field matches, following Unix cron semantics. + * + * @example + * `30 9 * * 1-5` runs at 09:30 every weekday. + * + * @category Automation State + */ +export interface AutomationSchedule { + /** Five-field AHP cron expression described by {@link AutomationSchedule}. */ expression: string; - /** IANA time-zone identifier. */ + /** + * IANA Time Zone Database identifier used to interpret the expression, for + * example `"UTC"` or `"Europe/Berlin"`. + */ timeZone: string; } -/** @category Automation State */ -export type AutomationSchedule = - | AutomationHourlySchedule - | AutomationDailySchedule - | AutomationWeeklySchedule - | AutomationCronSchedule; - -/** @category Automation State */ +/** + * How a host handles schedule occurrences missed while automatic execution was + * unavailable. + * + * @category Automation State + */ export const enum AutomationMisfirePolicy { + /** Discard missed occurrences and wait for the next future occurrence. */ Skip = 'skip', + /** + * Start at most one catch-up run when execution becomes available, regardless + * of how many occurrences were missed. + */ RunOnce = 'runOnce', } -/** @category Automation State */ +/** + * Discriminant for automatic trigger definitions. + * + * @category Automation State + */ export const enum AutomationTriggerKind { + /** A portable recurring {@link AutomationSchedule}. */ Schedule = 'schedule', + /** A host-defined external event discovered from trigger definitions. */ Event = 'event', } -/** @category Automation State */ +/** + * Starts runs from a recurring cron schedule evaluated by the host. + * + * @category Automation State + */ export interface AutomationScheduleTrigger { - /** Stable within the automation definition. */ + /** + * Identifier unique and stable within this automation definition. Run causes + * refer back to this value. + */ id: string; kind: AutomationTriggerKind.Schedule; + /** Recurrence and time zone evaluated by the host. */ schedule: AutomationSchedule; + /** + * Policy for missed occurrences. Omission is equivalent to + * {@link AutomationMisfirePolicy.RunOnce}. + */ misfirePolicy?: AutomationMisfirePolicy; } -/** @category Automation State */ +/** + * Starts runs from events understood by the owning host. + * + * Event trigger types, event ids, and configuration are discovered through + * `listAutomationTriggerDefinitions`. A client that does not understand a + * host-defined trigger can still preserve and display it without interpreting + * its configuration. + * + * @category Automation State + */ export interface AutomationEventTrigger { - /** Stable within the automation definition. */ + /** + * Identifier unique and stable within this automation definition. Run causes + * refer back to this value. + */ id: string; kind: AutomationTriggerKind.Event; - /** Stable host-defined trigger type. */ + /** Matches {@link AutomationTriggerDefinition.type}. */ type: string; - /** Selected event actions. */ + /** + * Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + * trigger type. + */ events: string[]; - /** Schema-defined values. Unknown entries must survive round-trips. */ + /** + * Values described by {@link AutomationTriggerDefinition.configSchema}. + * Clients MUST preserve unknown entries when editing other fields. + */ config?: Record; } -/** @category Automation State */ +/** + * An automatic cause that can create runs for an enabled automation. + * + * Manual execution is not represented as a trigger. An empty trigger list + * therefore means the automation is manual-only. + * + * @category Automation State + */ export type AutomationTrigger = | AutomationScheduleTrigger | AutomationEventTrigger; -/** @category Automation State */ +/** + * One selectable event exposed by a host-defined trigger type. + * + * @category Automation State + */ export interface AutomationTriggerEventDefinition { + /** Stable event id stored in {@link AutomationEventTrigger.events}. */ id: string; + /** Human-readable label suitable for selection UI. */ title: string; + /** Optional longer explanation of when this event fires. */ description?: string; } -/** @category Automation State */ +/** + * Describes one host-defined event trigger type available for a prospective + * automation session template. + * + * Trigger definitions are discovery metadata, not durable automation state. + * Hosts may return different definitions for different providers, working + * directories, or session configuration. + * + * @category Automation State + */ export interface AutomationTriggerDefinition { + /** Stable type id stored in {@link AutomationEventTrigger.type}. */ type: string; + /** Human-readable trigger type name. */ title: string; + /** Optional longer explanation of the trigger source. */ description?: string; + /** Events clients may select for this trigger type. */ events: AutomationTriggerEventDefinition[]; + /** Optional schema for {@link AutomationEventTrigger.config}. */ configSchema?: ConfigSchema; } -/** @category Automation State */ +/** + * Template from which the host creates a fresh session for each automation run. + * + * The host revalidates every selection when the run starts. Definitions never + * carry credentials, confirmation decisions, or durable permission grants. + * + * @category Automation State + */ export interface AutomationSessionTemplate { + /** Provider id. Omit to use the host's default provider. */ provider?: string; + /** Optional model selection resolved when a run starts. */ model?: ModelSelection; + /** Optional custom agent selection resolved when a run starts. */ agent?: AgentSelection; - /** Absence means a workspace-less session. */ + /** + * Ordered working-directory URIs for each created session. Absence means a + * workspace-less session. + */ workingDirectories?: URI[]; - /** Values resolved through `resolveSessionConfig`. */ + /** + * Session configuration values accepted by `createSession`, normally + * obtained from `resolveSessionConfig`. + */ config?: Record; } -/** @category Automation State */ +/** + * Durable, client-editable definition of an automation. + * + * A definition combines the initial user message, the session template used + * for each run, and zero or more automatic triggers. Runtime state, run + * history, revisions, timestamps, and currently allowed operations live on + * {@link AutomationState} rather than in the definition. + * + * @category Automation State + */ export interface AutomationDefinition { + /** Human-readable automation name. */ title: string; - /** Initial user message sent to each new session. */ + /** + * Initial message sent to every newly created run session. Its origin MUST be + * `user`. + */ message: Message; + /** Template used to create fresh sessions for each run. */ session: AutomationSessionTemplate; - /** Controls automatic triggers; manual runs remain permitted. */ + /** + * Whether automatic triggers may create runs. Manual runs remain available + * whenever {@link AutomationOperation.Run} is advertised. + */ enabled: boolean; - /** Empty means manual-only. */ + /** Automatic triggers. An empty list means manual-only. */ triggers: AutomationTrigger[]; + /** + * Opaque implementation-defined metadata. Clients MUST preserve unknown + * entries when updating the definition. + */ _meta?: Record; } -/** @category Automation State */ +/** + * Host-resolved execution context that is useful to clients but is not part of + * the editable definition. + * + * @category Automation State + */ export interface AutomationRuntimeState { + /** + * Effective working directories after host-side preparation, such as + * materializing a managed workspace. + */ workingDirectories?: URI[]; + /** Opaque host-defined runtime metadata. */ _meta?: Record; } -/** @category Automation State */ +/** + * Lightweight root-catalogue projection of an automation. + * + * Returned by `listAutomations` and carried by root automation notifications, + * this contains enough information to render a list without subscribing to + * every `ahp-automation:` resource. + * + * @category Automation State + */ export interface AutomationSummary { + /** Subscribable `ahp-automation:` URI. */ resource: URI; + /** Current {@link AutomationDefinition.title}. */ title: string; + /** Current {@link AutomationDefinition.enabled} value. */ enabled: boolean; + /** Number of automatic triggers in the current definition. */ triggerCount: number; + /** Earliest known future scheduled occurrence, as an ISO 8601 timestamp. */ nextRunAt?: string; + /** Most recent retained run, when any run exists. */ lastRun?: AutomationRunSummary; + /** Monotonic definition revision used for optimistic concurrency. */ revision: number; + /** Operations currently permitted for this automation. */ operations: AutomationOperation[]; + /** Creation timestamp in ISO 8601 format. */ createdAt: string; + /** Last definition modification timestamp in ISO 8601 format. */ modifiedAt: string; + /** Opaque host-defined catalogue metadata. */ _meta?: Record; } -/** @category Automation State */ +/** + * Authoritative state of one subscribed `ahp-automation:` resource. + * + * The host owns definition revisions, trigger evaluation, run claims, run + * retention, and operation availability. Clients render this state and submit + * commands; they never run a fallback scheduler for a host-owned definition. + * + * @category Automation State + */ export interface AutomationState { + /** URI of this automation channel. */ resource: URI; + /** Current durable definition. */ definition: AutomationDefinition; + /** + * Monotonically increasing definition revision. Clients pass the revision + * they observed as `updateAutomation.expectedRevision`. + */ revision: number; + /** Earliest known future scheduled occurrence, as an ISO 8601 timestamp. */ nextRunAt?: string; - /** Newest-first retained run summaries. */ + /** + * Newest-first retained run summaries. This is a bounded window; use + * `fetchAutomationRuns` when {@link runsNextCursor} is present. + */ runs: AutomationRunSummary[]; + /** Opaque cursor for the next older run-history page. */ runsNextCursor?: string; + /** Optional host-resolved execution context. */ runtime?: AutomationRuntimeState; + /** Operations currently permitted for this automation. */ operations: AutomationOperation[]; + /** Creation timestamp in ISO 8601 format. */ createdAt: string; + /** Last definition modification timestamp in ISO 8601 format. */ modifiedAt: string; + /** Opaque host-defined state metadata. */ _meta?: Record; } diff --git a/types/channels-root/notifications.ts b/types/channels-root/notifications.ts index 482a391b3..84642c87d 100644 --- a/types/channels-root/notifications.ts +++ b/types/channels-root/notifications.ts @@ -147,39 +147,60 @@ export interface SessionSummaryChangedParams { // ─── root/automationAdded ──────────────────────────────────────────────────── /** + * Announces a newly visible automation catalogue entry. + * + * Root notifications are live signals and are not replayed after reconnect. + * Clients that reconnect MUST refresh the catalogue with `listAutomations`. + * * @category Protocol Notifications * @method root/automationAdded * @direction Server → Client * @messageType Notification + * @version 1 */ export interface AutomationAddedParams { + /** Root channel URI. */ channel: URI; + /** Complete summary for the newly visible automation. */ summary: AutomationSummary; } // ─── root/automationRemoved ────────────────────────────────────────────────── /** + * Announces that an automation is no longer present in the root catalogue. + * * @category Protocol Notifications * @method root/automationRemoved * @direction Server → Client * @messageType Notification + * @version 1 */ export interface AutomationRemovedParams { + /** Root channel URI. */ channel: URI; + /** Removed `ahp-automation:` URI. */ automation: URI; } // ─── root/automationSummaryChanged ─────────────────────────────────────────── /** + * Replaces the root-catalogue summary for an existing automation. + * + * Full replacement semantics apply to `summary`; this is not a patch. The + * corresponding subscribed automation channel remains authoritative. + * * @category Protocol Notifications * @method root/automationSummaryChanged * @direction Server → Client * @messageType Notification + * @version 1 */ export interface AutomationSummaryChangedParams { + /** Root channel URI. */ channel: URI; + /** Complete replacement catalogue summary. */ summary: AutomationSummary; } diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 8055ce2c7..0340779db 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -59,19 +59,38 @@ export const enum SessionStatus { IsArchived = 1 << 6, } -/** @category Session State */ +/** + * Discriminant describing the durable provenance of a session. + * + * @category Session State + */ export const enum SessionOriginKind { + /** The session was created as part of an automation run. */ Automation = 'automation', } -/** @category Session State */ +/** + * Provenance recorded on a session created for an automation run. + * + * The links let clients navigate from an ordinary session to the task-level + * run and its durable definition. The session channel remains authoritative + * for this session's transcript, tools, confirmations, and changes. + * + * @category Session State + */ export interface AutomationSessionOrigin { kind: SessionOriginKind.Automation; + /** Owning `ahp-automation:` URI. */ automation: URI; + /** Owning `ahp-automation-run:` URI. */ run: URI; } -/** @category Session State */ +/** + * Durable provenance for sessions created by a higher-level AHP workflow. + * + * @category Session State + */ export type SessionOrigin = AutomationSessionOrigin; /** diff --git a/types/common/commands.ts b/types/common/commands.ts index d982dec40..daa88f32a 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -10,7 +10,7 @@ import type { URI, Snapshot } from './state.js'; import type { ActionEnvelope, StateAction } from './actions.js'; import type { TelemetryCapabilities } from '../channels-otlp/state.js'; -import type { AutomationExecutionLifetime, AutomationScheduleKind } from '../channels-automation/state.js'; +import type { AutomationExecutionLifetime } from '../channels-automation/state.js'; // ─── BaseParams ────────────────────────────────────────────────────────────── @@ -267,44 +267,98 @@ export interface InitializeResult { * @see {@link /specification/telemetry-channel | Telemetry Channel} */ telemetry?: TelemetryCapabilities; - /** Host automation support. Absence means unsupported. */ + /** + * Host-owned automation support. Absence means the host does not expose an + * automation catalogue or automation commands. + * + * @see {@link /guide/automations | Automations Guide} + */ automations?: AutomationCapabilities; } -/** @category Commands */ +/** + * Automation features supported by this host authority. + * + * Capabilities describe implementation support. Per-resource + * {@link AutomationState.operations} and + * {@link AutomationRunState.operations} remain authoritative for whether a + * particular operation is currently allowed. + * + * @category Commands + */ export interface AutomationCapabilities { + /** Availability guarantee for automatic trigger execution. */ execution: AutomationExecutionCapabilities; + /** Present when clients may call `createAutomation`. */ create?: AutomationCreateCapability; + /** Present when definitions may contain schedule triggers. */ schedules?: AutomationScheduleCapabilities; + /** Present when clients may request cancellation on eligible runs. */ runCancellation?: AutomationRunCancellationCapability; + /** Present when clients may call `previewAutomationSchedule`. */ schedulePreview?: AutomationSchedulePreviewCapability; + /** + * Maximum terminal run summaries retained per automation. Active runs are not + * counted toward the limit. Absence means the retention limit is + * implementation-defined. + */ runHistoryLimit?: number; } -/** @category Commands */ +/** + * Automatic trigger execution availability. + * + * @category Commands + */ export interface AutomationExecutionCapabilities { + /** How long automatic trigger evaluation remains available. */ lifetime: AutomationExecutionLifetime; } -/** @category Commands */ +/** + * Presence capability for `createAutomation`. + * + * The empty object means "supported"; fields are reserved for future + * create-specific options. + * + * @category Commands + */ export interface AutomationCreateCapability {} -/** @category Commands */ +/** + * Host restrictions on portable {@link AutomationSchedule} triggers. + * + * The cron grammar itself is fixed by AHP. Hosts MUST accept every expression + * in that grammar unless it violates an advertised interval restriction. + * + * @category Commands + */ export interface AutomationScheduleCapabilities { - kinds: AutomationScheduleKind[]; - cron?: AutomationCronScheduleCapability; -} - -/** @category Commands */ -export interface AutomationCronScheduleCapability { - dialect: 'unix5'; + /** + * Smallest permitted interval between consecutive occurrences. Omission + * means no restriction beyond the cron format's one-minute resolution. + */ minIntervalMinutes?: number; } -/** @category Commands */ +/** + * Presence capability for `automationRun/cancelRequested`. + * + * The empty object means "supported"; clients must additionally check for + * {@link AutomationRunOperation.Cancel} on each run. + * + * @category Commands + */ export interface AutomationRunCancellationCapability {} -/** @category Commands */ +/** + * Presence capability for `previewAutomationSchedule`. + * + * The empty object means "supported"; fields are reserved for future preview + * limits or options. + * + * @category Commands + */ export interface AutomationSchedulePreviewCapability {} // ─── ping ──────────────────────────────────────────────────────────────────── diff --git a/types/test-cases/round-trips/041-automation-snapshot.json b/types/test-cases/round-trips/041-automation-snapshot.json index bc43e5e5c..bbd8e1056 100644 --- a/types/test-cases/round-trips/041-automation-snapshot.json +++ b/types/test-cases/round-trips/041-automation-snapshot.json @@ -1,7 +1,7 @@ { "name": "automation-snapshot", "group": "A", - "description": "An automation snapshot preserves schedule and event triggers plus run summaries.", + "description": "An automation snapshot preserves cron and event triggers plus run summaries.", "type": "Snapshot", "input": { "resource": "ahp-automation:/a1", @@ -14,11 +14,10 @@ "enabled": true, "triggers": [ { - "id": "daily", + "id": "morning", "kind": "schedule", "schedule": { - "kind": "daily", - "time": { "hour": 9, "minute": 30 }, + "expression": "30 9 * * *", "timeZone": "Europe/Berlin" }, "misfirePolicy": "runOnce" @@ -59,11 +58,10 @@ "enabled": true, "triggers": [ { - "id": "daily", + "id": "morning", "kind": "schedule", "schedule": { - "kind": "daily", - "time": { "hour": 9, "minute": 30 }, + "expression": "30 9 * * *", "timeZone": "Europe/Berlin" }, "misfirePolicy": "runOnce" diff --git a/types/test-cases/round-trips/043-automation-capabilities.json b/types/test-cases/round-trips/043-automation-capabilities.json index fdc45808f..bf79da727 100644 --- a/types/test-cases/round-trips/043-automation-capabilities.json +++ b/types/test-cases/round-trips/043-automation-capabilities.json @@ -11,8 +11,7 @@ "execution": { "lifetime": "managed" }, "create": {}, "schedules": { - "kinds": ["hourly", "daily", "weekly", "cron"], - "cron": { "dialect": "unix5", "minIntervalMinutes": 5 } + "minIntervalMinutes": 5 }, "runCancellation": {}, "schedulePreview": {}, @@ -27,8 +26,7 @@ "execution": { "lifetime": "managed" }, "create": {}, "schedules": { - "kinds": ["hourly", "daily", "weekly", "cron"], - "cron": { "dialect": "unix5", "minIntervalMinutes": 5 } + "minIntervalMinutes": 5 }, "runCancellation": {}, "schedulePreview": {}, From 3a0627d3d6bb3e8e87f161a7038d8ad45231df76 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Thu, 13 Aug 2026 16:34:29 +0200 Subject: [PATCH 8/8] automations: feat: retain schedule cursors during import Require imported definitions to remain disabled through cutover and let clients supply the source scheduler's next unevaluated occurrence. Document durable occurrence claiming so catch-up runs cannot be duplicated after restart. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1131c2f6-bcd0-4012-9265-fdb2685179e7 --- clients/go/ahptypes/commands.generated.go | 25 +++++++++---- clients/go/ahptypes/state.generated.go | 4 +- .../generated/Commands.generated.kt | 25 +++++++++++-- .../generated/State.generated.kt | 4 +- clients/rust/crates/ahp-types/src/commands.rs | 28 ++++++++++---- clients/rust/crates/ahp-types/src/state.rs | 4 +- .../Generated/Commands.generated.swift | 30 ++++++++++++--- .../Generated/State.generated.swift | 4 +- docs/guide/automations.md | 20 +++++++--- schema/actions.schema.json | 4 +- schema/commands.schema.json | 37 ++++++++++++++++--- schema/errors.schema.json | 37 ++++++++++++++++--- schema/notifications.schema.json | 4 +- schema/state.schema.json | 4 +- scripts/generate-go.ts | 2 +- scripts/generate-kotlin.ts | 2 +- scripts/generate-rust.ts | 2 +- scripts/generate-swift.ts | 2 +- types/channels-automation/commands.ts | 31 ++++++++++++---- types/channels-automation/state.ts | 4 +- 20 files changed, 206 insertions(+), 67 deletions(-) diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index d57673eca..2507ddcab 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -1314,22 +1314,33 @@ type CreateAutomationParams struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Complete initial definition. Definition AutomationDefinition `json:"definition"` - // Optional idempotency identity when importing a legacy definition. - Import *AutomationImportIdentity `json:"import,omitempty"` + // Optional legacy import state. When present, {@link definition} MUST be + // disabled so automatic triggers cannot run before migration cutover. + Import *AutomationImport `json:"import,omitempty"` } -// Stable source identity used to make legacy automation import idempotent. +// Stable source identity and scheduler state for a legacy automation import. // -// The host remembers this identity independently of the client-chosen -// automation URI. Retrying an interrupted migration with the same values MUST -// resolve to the previously imported item rather than creating a duplicate. -type AutomationImportIdentity struct { +// The host remembers the identity independently of the client-chosen automation +// URI. Retrying with the same identity MUST resolve to the previously imported +// item rather than creating a duplicate. +type AutomationImport struct { // Stable namespace identifying the source implementation or store. Source string `json:"source"` // Identifier shared by every item in one import attempt. BatchId string `json:"batchId"` // Stable source-side identifier for this definition within the batch. ItemId string `json:"itemId"` + // Source schedule occurrences to retain until the imported definition is enabled. + TriggerNextRuns []AutomationImportTriggerNextRun `json:"triggerNextRuns,omitempty"` +} + +// Initial schedule occurrence retained while an imported automation is disabled. +type AutomationImportTriggerNextRun struct { + // Stable id of a schedule trigger in the imported definition. + TriggerId string `json:"triggerId"` + // Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp. + NextRunAt string `json:"nextRunAt"` } // Partial replacement of editable {@link AutomationDefinition} fields. diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index f39978e31..f60b94d47 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -3857,7 +3857,7 @@ type AutomationSummary struct { Enabled bool `json:"enabled"` // Number of automatic triggers in the current definition. TriggerCount int64 `json:"triggerCount"` - // Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + // Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. NextRunAt *string `json:"nextRunAt,omitempty"` // Most recent retained run, when any run exists. LastRun *AutomationRunSummary `json:"lastRun,omitempty"` @@ -3886,7 +3886,7 @@ type AutomationState struct { // Monotonically increasing definition revision. Clients pass the revision // they observed as `updateAutomation.expectedRevision`. Revision int64 `json:"revision"` - // Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + // Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. NextRunAt *string `json:"nextRunAt,omitempty"` // Newest-first retained run summaries. This is a bounded window; use // `fetchAutomationRuns` when {@link runsNextCursor} is present. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 608cf60d4..6e4c423f9 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -1655,14 +1655,15 @@ data class CreateAutomationParams( */ val definition: AutomationDefinition, /** - * Optional idempotency identity when importing a legacy definition. + * Optional legacy import state. When present, {@link definition} MUST be + * disabled so automatic triggers cannot run before migration cutover. */ @SerialName("import") - val `import`: AutomationImportIdentity? = null + val `import`: AutomationImport? = null ) @Serializable -data class AutomationImportIdentity( +data class AutomationImport( /** * Stable namespace identifying the source implementation or store. */ @@ -1674,7 +1675,23 @@ data class AutomationImportIdentity( /** * Stable source-side identifier for this definition within the batch. */ - val itemId: String + val itemId: String, + /** + * Source schedule occurrences to retain until the imported definition is enabled. + */ + val triggerNextRuns: List? = null +) + +@Serializable +data class AutomationImportTriggerNextRun( + /** + * Stable id of a schedule trigger in the imported definition. + */ + val triggerId: String, + /** + * Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp. + */ + val nextRunAt: String ) @Serializable 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 cdd1788f4..070fce747 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 @@ -5071,7 +5071,7 @@ data class AutomationSummary( */ val triggerCount: Long, /** - * Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + * Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. */ val nextRunAt: String? = null, /** @@ -5117,7 +5117,7 @@ data class AutomationState( */ val revision: Long, /** - * Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + * Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. */ val nextRunAt: String? = null, /** diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 8c5a3b3e3..e54fc1d1d 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -1603,25 +1603,39 @@ pub struct CreateAutomationParams { pub meta: Option, /// Complete initial definition. pub definition: AutomationDefinition, - /// Optional idempotency identity when importing a legacy definition. + /// Optional legacy import state. When present, {@link definition} MUST be + /// disabled so automatic triggers cannot run before migration cutover. #[serde(default, skip_serializing_if = "Option::is_none")] - pub import: Option, + pub import: Option, } -/// Stable source identity used to make legacy automation import idempotent. +/// Stable source identity and scheduler state for a legacy automation import. /// -/// The host remembers this identity independently of the client-chosen -/// automation URI. Retrying an interrupted migration with the same values MUST -/// resolve to the previously imported item rather than creating a duplicate. +/// The host remembers the identity independently of the client-chosen automation +/// URI. Retrying with the same identity MUST resolve to the previously imported +/// item rather than creating a duplicate. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AutomationImportIdentity { +pub struct AutomationImport { /// Stable namespace identifying the source implementation or store. pub source: String, /// Identifier shared by every item in one import attempt. pub batch_id: String, /// Stable source-side identifier for this definition within the batch. pub item_id: String, + /// Source schedule occurrences to retain until the imported definition is enabled. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub trigger_next_runs: Option>, +} + +/// Initial schedule occurrence retained while an imported automation is disabled. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationImportTriggerNextRun { + /// Stable id of a schedule trigger in the imported definition. + pub trigger_id: String, + /// Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp. + pub next_run_at: String, } /// Partial replacement of editable {@link AutomationDefinition} fields. diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 328927d36..5dbc4180f 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -4604,7 +4604,7 @@ pub struct AutomationSummary { pub enabled: bool, /// Number of automatic triggers in the current definition. pub trigger_count: i64, - /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + /// Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. #[serde(default, skip_serializing_if = "Option::is_none")] pub next_run_at: Option, /// Most recent retained run, when any run exists. @@ -4638,7 +4638,7 @@ pub struct AutomationState { /// Monotonically increasing definition revision. Clients pass the revision /// they observed as `updateAutomation.expectedRevision`. pub revision: i64, - /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + /// Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. #[serde(default, skip_serializing_if = "Option::is_none")] pub next_run_at: Option, /// Newest-first retained run summaries. This is a bounded window; use diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 78f26f2b1..6f24c6982 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -2018,8 +2018,9 @@ public struct CreateAutomationParams: Codable, Sendable { public var meta: [String: AnyCodable]? /// Complete initial definition. public var definition: AutomationDefinition - /// Optional idempotency identity when importing a legacy definition. - public var `import`: AutomationImportIdentity? + /// Optional legacy import state. When present, {@link definition} MUST be + /// disabled so automatic triggers cannot run before migration cutover. + public var `import`: AutomationImport? enum CodingKeys: String, CodingKey { case channel @@ -2032,7 +2033,7 @@ public struct CreateAutomationParams: Codable, Sendable { channel: String, meta: [String: AnyCodable]? = nil, definition: AutomationDefinition, - `import`: AutomationImportIdentity? = nil + `import`: AutomationImport? = nil ) { self.channel = channel self.meta = meta @@ -2041,22 +2042,41 @@ public struct CreateAutomationParams: Codable, Sendable { } } -public struct AutomationImportIdentity: Codable, Sendable { +public struct AutomationImport: Codable, Sendable { /// Stable namespace identifying the source implementation or store. public var source: String /// Identifier shared by every item in one import attempt. public var batchId: String /// Stable source-side identifier for this definition within the batch. public var itemId: String + /// Source schedule occurrences to retain until the imported definition is enabled. + public var triggerNextRuns: [AutomationImportTriggerNextRun]? public init( source: String, batchId: String, - itemId: String + itemId: String, + triggerNextRuns: [AutomationImportTriggerNextRun]? = nil ) { self.source = source self.batchId = batchId self.itemId = itemId + self.triggerNextRuns = triggerNextRuns + } +} + +public struct AutomationImportTriggerNextRun: Codable, Sendable { + /// Stable id of a schedule trigger in the imported definition. + public var triggerId: String + /// Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp. + public var nextRunAt: String + + public init( + triggerId: String, + nextRunAt: String + ) { + self.triggerId = triggerId + self.nextRunAt = nextRunAt } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 37f69cb1d..9993577c5 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -5576,7 +5576,7 @@ public struct AutomationSummary: Codable, Sendable { public var enabled: Bool /// Number of automatic triggers in the current definition. public var triggerCount: Int - /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + /// Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. public var nextRunAt: String? /// Most recent retained run, when any run exists. public var lastRun: AutomationRunSummary? @@ -5640,7 +5640,7 @@ public struct AutomationState: Codable, Sendable { /// Monotonically increasing definition revision. Clients pass the revision /// they observed as `updateAutomation.expectedRevision`. public var revision: Int - /// Earliest known future scheduled occurrence, as an ISO 8601 timestamp. + /// Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. public var nextRunAt: String? /// Newest-first retained run summaries. This is a bounded window; use /// `fetchAutomationRuns` when {@link runsNextCursor} is present. diff --git a/docs/guide/automations.md b/docs/guide/automations.md index 64e239adc..5faa92793 100644 --- a/docs/guide/automations.md +++ b/docs/guide/automations.md @@ -328,19 +328,28 @@ full rather than merging recursively. ### Idempotent migration imports `createAutomation.import` carries a stable source, batch, and item identity for -legacy migration: +legacy migration. It may also carry the source scheduler's next unevaluated +occurrence for each schedule trigger: ```typescript { source: 'legacy-client-store' batchId: 'migration-2026-08-12' itemId: 'automation-42' + triggerNextRuns: [{ + triggerId: 'weekday-morning' + nextRunAt: '2026-08-12T07:00:00Z' + }] } ``` Retrying an interrupted migration with the same identity resolves to the -already imported item rather than creating a duplicate. Migration should move -one definition to exactly one authority: +already imported item rather than creating a duplicate. An imported definition +must be created disabled. The host persists supplied trigger occurrences while +the definition is disabled; after cutover, enabling it evaluates an overdue +occurrence according to that trigger's misfire policy. + +Migration should move one definition to exactly one authority: 1. Create the host definition disabled. 2. Verify the imported definition. @@ -348,7 +357,9 @@ one definition to exactly one authority: 4. Enable the host definition. Never leave both copies schedulable and never deduplicate definitions by -content; identical-looking automations may be intentional. +content; identical-looking automations may be intentional. The host must +durably claim a due occurrence together with its run record before starting +external execution, so restart cannot dispatch the same catch-up twice. ## Runs @@ -478,4 +489,3 @@ applications. - Event provenance and `_meta` values must not contain secrets. - Linked session channels use the ordinary AHP confirmation, authentication, and client-execution mechanisms. - diff --git a/schema/actions.schema.json b/schema/actions.schema.json index eea8329c4..351169e11 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -7570,7 +7570,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "lastRun": { "$ref": "#/$defs/AutomationRunSummary", @@ -7630,7 +7630,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "runs": { "type": "array", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 6d3c0154a..38d56306b 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -1679,9 +1679,27 @@ "items" ] }, - "AutomationImportIdentity": { + "AutomationImportTriggerNextRun": { "type": "object", - "description": "Stable source identity used to make legacy automation import idempotent.\n\nThe host remembers this identity independently of the client-chosen\nautomation URI. Retrying an interrupted migration with the same values MUST\nresolve to the previously imported item rather than creating a duplicate.", + "description": "Initial schedule occurrence retained while an imported automation is disabled.", + "properties": { + "triggerId": { + "type": "string", + "description": "Stable id of a schedule trigger in the imported definition." + }, + "nextRunAt": { + "type": "string", + "description": "Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp." + } + }, + "required": [ + "triggerId", + "nextRunAt" + ] + }, + "AutomationImport": { + "type": "object", + "description": "Stable source identity and scheduler state for a legacy automation import.\n\nThe host remembers the identity independently of the client-chosen automation\nURI. Retrying with the same identity MUST resolve to the previously imported\nitem rather than creating a duplicate.", "properties": { "source": { "type": "string", @@ -1694,6 +1712,13 @@ "itemId": { "type": "string", "description": "Stable source-side identifier for this definition within the batch." + }, + "triggerNextRuns": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationImportTriggerNextRun" + }, + "description": "Source schedule occurrences to retain until the imported definition is enabled." } }, "required": [ @@ -1720,8 +1745,8 @@ "description": "Complete initial definition." }, "import": { - "$ref": "#/$defs/AutomationImportIdentity", - "description": "Optional idempotency identity when importing a legacy definition." + "$ref": "#/$defs/AutomationImport", + "description": "Optional legacy import state. When present, {@link definition} MUST be\ndisabled so automatic triggers cannot run before migration cutover." } }, "required": [ @@ -7089,7 +7114,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "lastRun": { "$ref": "#/$defs/AutomationRunSummary", @@ -7149,7 +7174,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "runs": { "type": "array", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 8ac50f0d6..bbbf94140 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -5265,7 +5265,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "lastRun": { "$ref": "#/$defs/AutomationRunSummary", @@ -5325,7 +5325,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "runs": { "type": "array", @@ -7411,9 +7411,27 @@ "items" ] }, - "AutomationImportIdentity": { + "AutomationImportTriggerNextRun": { "type": "object", - "description": "Stable source identity used to make legacy automation import idempotent.\n\nThe host remembers this identity independently of the client-chosen\nautomation URI. Retrying an interrupted migration with the same values MUST\nresolve to the previously imported item rather than creating a duplicate.", + "description": "Initial schedule occurrence retained while an imported automation is disabled.", + "properties": { + "triggerId": { + "type": "string", + "description": "Stable id of a schedule trigger in the imported definition." + }, + "nextRunAt": { + "type": "string", + "description": "Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp." + } + }, + "required": [ + "triggerId", + "nextRunAt" + ] + }, + "AutomationImport": { + "type": "object", + "description": "Stable source identity and scheduler state for a legacy automation import.\n\nThe host remembers the identity independently of the client-chosen automation\nURI. Retrying with the same identity MUST resolve to the previously imported\nitem rather than creating a duplicate.", "properties": { "source": { "type": "string", @@ -7426,6 +7444,13 @@ "itemId": { "type": "string", "description": "Stable source-side identifier for this definition within the batch." + }, + "triggerNextRuns": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationImportTriggerNextRun" + }, + "description": "Source schedule occurrences to retain until the imported definition is enabled." } }, "required": [ @@ -7452,8 +7477,8 @@ "description": "Complete initial definition." }, "import": { - "$ref": "#/$defs/AutomationImportIdentity", - "description": "Optional idempotency identity when importing a legacy definition." + "$ref": "#/$defs/AutomationImport", + "description": "Optional legacy import state. When present, {@link definition} MUST be\ndisabled so automatic triggers cannot run before migration cutover." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 6bf52d285..07a10bcc2 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -5495,7 +5495,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "lastRun": { "$ref": "#/$defs/AutomationRunSummary", @@ -5555,7 +5555,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "runs": { "type": "array", diff --git a/schema/state.schema.json b/schema/state.schema.json index 7f0e1de69..ef5eadd4b 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -5176,7 +5176,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "lastRun": { "$ref": "#/$defs/AutomationRunSummary", @@ -5236,7 +5236,7 @@ }, "nextRunAt": { "type": "string", - "description": "Earliest known future scheduled occurrence, as an ISO 8601 timestamp." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, "runs": { "type": "array", diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 393ced899..206fb6a6d 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1723,7 +1723,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'ChangesetOperationFollowUp' }, { name: 'ListAutomationsParams' }, { name: 'ListAutomationsResult' }, { name: 'ListAutomationTriggerDefinitionsParams' }, { name: 'ListAutomationTriggerDefinitionsResult' }, - { name: 'CreateAutomationParams' }, { name: 'AutomationImportIdentity' }, { name: 'AutomationDefinitionPatch' }, + { name: 'CreateAutomationParams' }, { name: 'AutomationImport' }, { name: 'AutomationImportTriggerNextRun' }, { name: 'AutomationDefinitionPatch' }, { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index afc226943..51abb4707 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1689,7 +1689,7 @@ const COMMAND_STRUCTS = [ 'ChangesetOperationFollowUp', 'ListAutomationsParams', 'ListAutomationsResult', 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', - 'CreateAutomationParams', 'AutomationImportIdentity', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'CreateAutomationParams', 'AutomationImport', 'AutomationImportTriggerNextRun', 'AutomationDefinitionPatch', 'UpdateAutomationParams', 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 093a9d1fb..79d2bf2fd 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1561,7 +1561,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'ChangesetOperationFollowUp' }, { name: 'ListAutomationsParams' }, { name: 'ListAutomationsResult' }, { name: 'ListAutomationTriggerDefinitionsParams' }, { name: 'ListAutomationTriggerDefinitionsResult' }, - { name: 'CreateAutomationParams' }, { name: 'AutomationImportIdentity' }, { name: 'AutomationDefinitionPatch' }, + { name: 'CreateAutomationParams' }, { name: 'AutomationImport' }, { name: 'AutomationImportTriggerNextRun' }, { name: 'AutomationDefinitionPatch' }, { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 6b731b1a5..02dfb7b46 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1582,7 +1582,7 @@ const COMMAND_STRUCTS = [ 'ChangesetOperationFollowUp', 'ListAutomationsParams', 'ListAutomationsResult', 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', - 'CreateAutomationParams', 'AutomationImportIdentity', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'CreateAutomationParams', 'AutomationImport', 'AutomationImportTriggerNextRun', 'AutomationDefinitionPatch', 'UpdateAutomationParams', 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', diff --git a/types/channels-automation/commands.ts b/types/channels-automation/commands.ts index 2ee8ce7ef..e6f10e53e 100644 --- a/types/channels-automation/commands.ts +++ b/types/channels-automation/commands.ts @@ -83,21 +83,35 @@ export interface ListAutomationTriggerDefinitionsResult { } /** - * Stable source identity used to make legacy automation import idempotent. + * Initial schedule occurrence retained while an imported automation is disabled. * - * The host remembers this identity independently of the client-chosen - * automation URI. Retrying an interrupted migration with the same values MUST - * resolve to the previously imported item rather than creating a duplicate. + * @category Commands + */ +export interface AutomationImportTriggerNextRun { + /** Stable id of a schedule trigger in the imported definition. */ + triggerId: string; + /** Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp. */ + nextRunAt: string; +} + +/** + * Stable source identity and scheduler state for a legacy automation import. + * + * The host remembers the identity independently of the client-chosen automation + * URI. Retrying with the same identity MUST resolve to the previously imported + * item rather than creating a duplicate. * * @category Commands */ -export interface AutomationImportIdentity { +export interface AutomationImport { /** Stable namespace identifying the source implementation or store. */ source: string; /** Identifier shared by every item in one import attempt. */ batchId: string; /** Stable source-side identifier for this definition within the batch. */ itemId: string; + /** Source schedule occurrences to retain until the imported definition is enabled. */ + triggerNextRuns?: AutomationImportTriggerNextRun[]; } /** @@ -119,8 +133,11 @@ export interface CreateAutomationParams extends BaseParams { channel: URI; /** Complete initial definition. */ definition: AutomationDefinition; - /** Optional idempotency identity when importing a legacy definition. */ - import?: AutomationImportIdentity; + /** + * Optional legacy import state. When present, {@link definition} MUST be + * disabled so automatic triggers cannot run before migration cutover. + */ + import?: AutomationImport; } /** diff --git a/types/channels-automation/state.ts b/types/channels-automation/state.ts index 8a5810bb9..65dfa2ae3 100644 --- a/types/channels-automation/state.ts +++ b/types/channels-automation/state.ts @@ -317,7 +317,7 @@ export interface AutomationSummary { enabled: boolean; /** Number of automatic triggers in the current definition. */ triggerCount: number; - /** Earliest known future scheduled occurrence, as an ISO 8601 timestamp. */ + /** Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. */ nextRunAt?: string; /** Most recent retained run, when any run exists. */ lastRun?: AutomationRunSummary; @@ -352,7 +352,7 @@ export interface AutomationState { * they observed as `updateAutomation.expectedRevision`. */ revision: number; - /** Earliest known future scheduled occurrence, as an ISO 8601 timestamp. */ + /** Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. */ nextRunAt?: string; /** * Newest-first retained run summaries. This is a bounded window; use