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..f5febed69 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,39 @@ 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 + if capabilities.Schedules.MinIntervalMinutes != nil { + minIntervalMinutes := *capabilities.Schedules.MinIntervalMinutes + value.MinIntervalMinutes = &minIntervalMinutes + } + 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 d8bff27a0..db6f1f0e1 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -1815,3 +1815,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 8e8d296e7..b0810c80a 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 ───────────────────────────────────────────────── @@ -1484,6 +1495,125 @@ 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"` + // Complete replacement definition. + Definition AutomationDefinition `json:"definition"` + // 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"` + // 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"` + // {@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"` + // 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"` + // 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 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"` + // 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"` + // 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"` + // 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"` + // {@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"` +} + // ─── StateAction Union ─────────────────────────────────────────────── // StateAction is the discriminated union of every state action. @@ -1495,91 +1625,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 { @@ -2105,6 +2246,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/commands.generated.go b/clients/go/ahptypes/commands.generated.go index d7545ffc1..2507ddcab 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -167,6 +167,9 @@ 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-owned automation support. Absence means the host does not expose an + // automation catalogue or automation commands. + Automations *AutomationCapabilities `json:"automations,omitempty"` } // Optional capabilities a client declares during `initialize`. @@ -189,6 +192,66 @@ 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 { + // 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"` + // 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 { + // 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 { +} + // 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 +1240,224 @@ 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"` + // 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. + 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"` + // 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"` + // 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"` + // 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"` + // Complete initial definition. + Definition AutomationDefinition `json:"definition"` + // 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 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. +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. +// +// Omitted fields are unchanged. Supplied arrays and objects replace their +// corresponding values in full; they are not merged recursively. +type AutomationDefinitionPatch struct { + // 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"` + // 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"` + // Optional JSON-serializable metadata associated with this request. + // Receivers MUST ignore keys they do not understand. + 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"` + // 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 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"` + // 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"` +} + 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 dc600b869..0f92d43d4 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -85,6 +85,36 @@ 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 { + // 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 { + // 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 { + // Root channel URI. + Channel URI `json:"channel"` + // Complete replacement catalogue summary. + 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 +245,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 d53d41063..f0ba63e2a 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -451,6 +451,128 @@ 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 ( + // Replace editable fields using `updateAutomation`. + AutomationOperationUpdate AutomationOperation = "update" + // Permanently remove the automation using `disposeAutomation`. + AutomationOperationDispose AutomationOperation = "dispose" + // 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" + // 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 ( + // 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" + // 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 ( + // 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" + // 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 ( + // 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" + // 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 ( + // 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" +) + // ─── Structs ────────────────────────────────────────────────────────── // An optionally-sized icon that can be displayed in a user interface. @@ -757,6 +879,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 @@ -1031,6 +1155,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 @@ -3597,6 +3723,400 @@ 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"` + // Owning `ahp-automation:` URI. + Automation URI `json:"automation"` + // Owning `ahp-automation-run:` URI. + Run URI `json:"run"` +} + +// 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 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 { + // 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 { + // Identifier unique and stable within this automation definition. Run causes + // refer back to this value. + Id string `json:"id"` + Kind AutomationTriggerKind `json:"kind"` + // Matches {@link AutomationTriggerDefinition.type}. + Type string `json:"type"` + // Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + // trigger type. + Events []string `json:"events"` + // 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 { + // 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 { + // 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 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"` + // 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 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"` + // Whether automatic triggers may create runs. Manual runs remain available + // whenever {@link AutomationOperation.Run} is advertised. + Enabled bool `json:"enabled"` + // 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 { + // 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 { + // 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 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"` + // 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 { + // URI of this automation channel. + Resource URI `json:"resource"` + // Current durable definition. + Definition AutomationDefinition `json:"definition"` + // Monotonically increasing definition revision. Clients pass the revision + // they observed as `updateAutomation.expectedRevision`. + Revision int64 `json:"revision"` + // 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. + 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"` + // 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"` + // 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"` + // 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"` + // 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"` + // 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"` + // 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"` + // 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"` + // Approximate size in bytes + SizeHint *int64 `json:"sizeHint,omitempty"` + // Content MIME type + ContentType *string `json:"contentType,omitempty"` + // Content nonce + 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 { + // 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 { + // 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"` +} + // ─── Customization Enablement Union ─────────────────────────────────────── // CustomizationEnablement is a single explicit customization enablement decision. @@ -4968,6 +5488,281 @@ 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) +} + +// 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 @@ -5065,10 +5860,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:"-"` @@ -5077,11 +5874,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: @@ -5110,6 +5913,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 b23d47d41..dc3de0ba8 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 ───────────────────────────────────────────────────── @@ -1777,3 +1789,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 b28cc47d1..a5beea101 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,121 @@ data class ResourceWatchChangedAction( val changes: JsonElement ) +@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 +) + +@Serializable +data class AutomationRunCancelRequestedAction( + val type: ActionType +) + // ─── Partial Summary Types ────────────────────────────────────────────────── @Serializable @@ -1622,6 +1760,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 +1871,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 +1975,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..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 @@ -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,12 @@ 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-owned automation support. Absence means the host does not expose an + * automation catalogue or automation commands. + */ + val automations: AutomationCapabilities? = null ) @Serializable @@ -360,6 +366,62 @@ data class ClientCapabilities( val mcpApps: Map? = null ) +@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 +) + +@Serializable +class AutomationCreateCapability + +@Serializable +data class AutomationScheduleCapabilities( + /** + * Smallest permitted interval between consecutive occurrences. Omission + * means no restriction beyond the cron format's one-minute resolution. + */ + val minIntervalMinutes: Long? = null +) + +@Serializable +class AutomationRunCancellationCapability + +@Serializable +class AutomationSchedulePreviewCapability + @Serializable data class Implementation( /** @@ -1497,6 +1559,286 @@ data class ChangesetOperationFollowUp( val external: Boolean? = null ) +@Serializable +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. + * 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, + /** + * Optional exact filter on {@link AutomationDefinition.enabled}. + */ + 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, + /** + * Automation summaries in host-defined catalogue order. + */ + val items: List +) + +@Serializable +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, + /** + * 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 +) + +@Serializable +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, + /** + * Complete initial definition. + */ + val definition: AutomationDefinition, + /** + * Optional legacy import state. When present, {@link definition} MUST be + * disabled so automatic triggers cannot run before migration cutover. + */ + @SerialName("import") + val `import`: AutomationImport? = null +) + +@Serializable +data class AutomationImport( + /** + * 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, + /** + * 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 +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 +) + +@Serializable +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, + /** + * Revision on which the client based {@link changes}. + */ + val expectedRevision: Long, + /** + * Editable fields to replace. + */ + val changes: AutomationDefinitionPatch +) + +@Serializable +data class DisposeAutomationParams( + /** + * 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 +) + +@Serializable +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, + /** + * 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 +) + +@Serializable +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, + /** + * Cursor previously received as {@link AutomationState.runsNextCursor}. + * Omit to request the first page not already included by the snapshot. + */ + val cursor: String? = null +) + +@Serializable +class FetchAutomationRunsResult + +@Serializable +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, + /** + * 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 +) + // ─── 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 8797fcdc3..77831fc92 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,42 @@ data class SessionSummaryChangedParams( val changes: PartialSessionSummary ) +@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 +) + @Serializable data class ProgressParams( /** @@ -191,6 +228,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 81c458561..cb635881a 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 ─────────────────────────────────────────────────────────── @@ -763,6 +764,206 @@ 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 +} + +/** + * 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 +} + // ─── State Types ──────────────────────────────────────────────────────────── @Serializable @@ -1342,6 +1543,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 */ @@ -1619,6 +1824,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 */ @@ -4710,77 +4919,609 @@ data class ResourceChange( val type: ResourceChangeType ) -// ─── Customization Enablement Union ───────────────────────────────────── - -/** - * A single explicit customization enablement decision. - */ -@Serializable(with = CustomizationEnablementSerializer::class) -sealed interface CustomizationEnablement { - @JvmInline value class Global(val value: CustomizationEnablementGlobal) : CustomizationEnablement - @JvmInline value class Workspace(val value: CustomizationEnablementWorkspace) : CustomizationEnablement - @JvmInline value class Session(val value: CustomizationEnablementSession) : CustomizationEnablement -} +@Serializable +data class AutomationSessionOrigin( + val kind: SessionOriginKind, + /** + * Owning `ahp-automation:` URI. + */ + val automation: String, + /** + * Owning `ahp-automation-run:` URI. + */ + val run: String +) @Serializable -data class CustomizationEnablementGlobal( - val enabled: Boolean, - val kind: String = "global", +data class AutomationSchedule( + /** + * Five-field AHP cron expression described by {@link AutomationSchedule}. + */ + val expression: String, + /** + * IANA Time Zone Database identifier used to interpret the expression, for + * example `"UTC"` or `"Europe/Berlin"`. + */ + val timeZone: String ) @Serializable -data class CustomizationEnablementWorkspace( - val uri: URI, - val enabled: Boolean, - val kind: String = "workspace", +data class AutomationScheduleTrigger( + /** + * 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 CustomizationEnablementSession( - val enabled: Boolean, - val kind: String = "session", +data class AutomationEventTrigger( + /** + * Identifier unique and stable within this automation definition. Run causes + * refer back to this value. + */ + val id: String, + val kind: AutomationTriggerKind, + /** + * Matches {@link AutomationTriggerDefinition.type}. + */ + val type: String, + /** + * Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + * trigger type. + */ + val events: List, + /** + * Values described by {@link AutomationTriggerDefinition.configSchema}. + * Clients MUST preserve unknown entries when editing other fields. + */ + val config: Map? = null ) -internal object CustomizationEnablementSerializer : KSerializer { - override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("CustomizationEnablement") +@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 +) - override fun deserialize(decoder: Decoder): CustomizationEnablement { - val input = decoder as? JsonDecoder - ?: error("CustomizationEnablement can only be deserialized from JSON") - val element = input.decodeJsonElement() - val obj = element as? JsonObject - ?: error("Expected JsonObject for CustomizationEnablement") - return when ((obj["kind"] as? JsonPrimitive)?.contentOrNull) { - "global" -> CustomizationEnablement.Global( - input.json.decodeFromJsonElement(CustomizationEnablementGlobal.serializer(), element), - ) - "workspace" -> CustomizationEnablement.Workspace( - input.json.decodeFromJsonElement(CustomizationEnablementWorkspace.serializer(), element), - ) - "session" -> CustomizationEnablement.Session( - input.json.decodeFromJsonElement(CustomizationEnablementSession.serializer(), element), - ) - else -> error("Unknown CustomizationEnablement kind") - } - } +@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 +) - override fun serialize(encoder: Encoder, value: CustomizationEnablement) { - val output = encoder as? JsonEncoder - ?: error("CustomizationEnablement can only be serialized to JSON") - val element: JsonElement = when (value) { - is CustomizationEnablement.Global -> - output.json.encodeToJsonElement(CustomizationEnablementGlobal.serializer(), value.value) - is CustomizationEnablement.Workspace -> - output.json.encodeToJsonElement(CustomizationEnablementWorkspace.serializer(), value.value) - is CustomizationEnablement.Session -> - output.json.encodeToJsonElement(CustomizationEnablementSession.serializer(), value.value) - } - output.encodeJsonElement(element) - } -} +@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, + /** + * Ordered working-directory URIs for each created session. Absence means a + * workspace-less session. + */ + val workingDirectories: List? = null, + /** + * Session configuration values accepted by `createSession`, normally + * obtained from `resolveSessionConfig`. + */ + val config: Map? = null +) -// ─── Tool Input ────────────────────────────────────────────────────────────── +@Serializable +data class AutomationDefinition( + /** + * Human-readable automation name. + */ + val title: String, + /** + * 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, + /** + * Whether automatic triggers may create runs. Manual runs remain available + * whenever {@link AutomationOperation.Run} is advertised. + */ + val enabled: Boolean, + /** + * 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 schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. + */ + 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 schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending. + */ + val nextRunAt: String? = null, + /** + * 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 +) + +@Serializable +data class AutomationManualRunCause( + val kind: AutomationRunCauseKind +) + +@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, non-secret event provenance suitable for display or audit. + * This is descriptive context, not an input that clients replay. + */ + val event: Map? = null +) + +@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 +) + +@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, + /** + * 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 +) + +// ─── Customization Enablement Union ───────────────────────────────────── + +/** + * A single explicit customization enablement decision. + */ +@Serializable(with = CustomizationEnablementSerializer::class) +sealed interface CustomizationEnablement { + @JvmInline value class Global(val value: CustomizationEnablementGlobal) : CustomizationEnablement + @JvmInline value class Workspace(val value: CustomizationEnablementWorkspace) : CustomizationEnablement + @JvmInline value class Session(val value: CustomizationEnablementSession) : CustomizationEnablement +} + +@Serializable +data class CustomizationEnablementGlobal( + val enabled: Boolean, + val kind: String = "global", +) + +@Serializable +data class CustomizationEnablementWorkspace( + val uri: URI, + val enabled: Boolean, + val kind: String = "workspace", +) + +@Serializable +data class CustomizationEnablementSession( + val enabled: Boolean, + val kind: String = "session", +) + +internal object CustomizationEnablementSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("CustomizationEnablement") + + override fun deserialize(decoder: Decoder): CustomizationEnablement { + val input = decoder as? JsonDecoder + ?: error("CustomizationEnablement can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for CustomizationEnablement") + return when ((obj["kind"] as? JsonPrimitive)?.contentOrNull) { + "global" -> CustomizationEnablement.Global( + input.json.decodeFromJsonElement(CustomizationEnablementGlobal.serializer(), element), + ) + "workspace" -> CustomizationEnablement.Workspace( + input.json.decodeFromJsonElement(CustomizationEnablementWorkspace.serializer(), element), + ) + "session" -> CustomizationEnablement.Session( + input.json.decodeFromJsonElement(CustomizationEnablementSession.serializer(), element), + ) + else -> error("Unknown CustomizationEnablement kind") + } + } + + override fun serialize(encoder: Encoder, value: CustomizationEnablement) { + val output = encoder as? JsonEncoder + ?: error("CustomizationEnablement can only be serialized to JSON") + val element: JsonElement = when (value) { + is CustomizationEnablement.Global -> + output.json.encodeToJsonElement(CustomizationEnablementGlobal.serializer(), value.value) + is CustomizationEnablement.Workspace -> + output.json.encodeToJsonElement(CustomizationEnablementWorkspace.serializer(), value.value) + is CustomizationEnablement.Session -> + output.json.encodeToJsonElement(CustomizationEnablementSession.serializer(), value.value) + } + output.encodeJsonElement(element) + } +} + +// ─── Tool Input ────────────────────────────────────────────────────────────── /** * Raw tool input represented inline or by content reference. @@ -5794,6 +6535,197 @@ 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 = 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 @@ -5852,8 +6784,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 { @@ -5864,6 +6795,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 { @@ -5876,7 +6809,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` @@ -5884,6 +6819,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") -> @@ -5909,6 +6848,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/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 e4369dda1..e71f5fbd9 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -13,10 +13,11 @@ 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, CustomizationEnablement, ErrorInfo, + AgentInfo, AgentSelection, Annotation, AnnotationEntry, AutomationDefinition, + AutomationRunArtifact, AutomationRunLifecycle, AutomationRunOperation, AutomationRunSummary, + Changeset, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, + ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, + ChatSummary, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, McpAuthRequirement, McpServerState, Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, @@ -199,6 +200,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 ───────────────────────────────────────────────── @@ -1741,6 +1764,138 @@ 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 {} + // ─── Partial Summaries ──────────────────────────────────────────────── /// Partial equivalent of ChatSummary — every field is optional for delta updates. @@ -1954,6 +2109,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..e54fc1d1d 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -15,7 +15,9 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::actions::{ActionEnvelope, StateAction}; #[allow(unused_imports)] use crate::state::{ - AgentSelection, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, + AgentSelection, AutomationDefinition, AutomationExecutionLifetime, AutomationSchedule, + AutomationSessionTemplate, AutomationSummary, AutomationTrigger, AutomationTriggerDefinition, + ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn, }; @@ -195,6 +197,10 @@ pub struct InitializeResult { /// filtering). Clients MAY ignore signals they cannot process. #[serde(default, skip_serializing_if = "Option::is_none")] pub telemetry: Option, + /// 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, } /// Optional capabilities a client declares during `initialize`. @@ -220,6 +226,81 @@ 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 {} + +/// 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 { + /// 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 {} + /// 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 +1509,280 @@ 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 { + /// 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. + #[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, + /// 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 { + /// 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, + /// 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 { + /// 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, + /// 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 { + /// 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, + /// Complete initial definition. + pub definition: AutomationDefinition, + /// 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, +} + +/// 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. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +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. +/// +/// 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 { + /// 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, + /// 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 { + /// 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, +} + +/// 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 { + /// 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, + /// 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 { + /// 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, + /// 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 { + /// 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, + /// 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, +} + // ─── 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 fa17881b1..9b0e85dc7 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, ProtectedResourceMetadata, SessionStatus, SessionSummary, + AgentSelection, AnnotationsSummary, AutomationOperation, AutomationRunSummary, + AutomationSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, + ProtectedResourceMetadata, SessionOrigin, SessionStatus, SessionSummary, }; // ─── Enums ──────────────────────────────────────────────────────────── @@ -96,6 +97,42 @@ 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, +} + /// Generic progress notification for a long-running operation. /// /// A client opts in to progress for a request by including a `progressToken` in @@ -245,6 +282,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 44f0ea48c..ed1c1d8cb 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -598,6 +598,142 @@ 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, +} + +/// 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, +} + // ─── Structs ────────────────────────────────────────────────────────── /// An optionally-sized icon that can be displayed in a user interface. @@ -1161,6 +1297,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, @@ -1457,6 +1596,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, @@ -4305,6 +4447,469 @@ 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 AutomationSchedule { + /// Five-field AHP cron expression described by {@link AutomationSchedule}. + pub expression: String, + /// 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 { + /// 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 { + /// Identifier unique and stable within this automation definition. Run causes + /// refer back to this value. + pub id: String, + /// Matches {@link AutomationTriggerDefinition.type}. + pub r#type: String, + /// Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + /// trigger type. + pub events: Vec, + /// 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, + /// 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>, + /// 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 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, + /// Whether automatic triggers may create runs. Manual runs remain available + /// whenever {@link AutomationOperation.Run} is advertised. + pub enabled: bool, + /// 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 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. + #[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 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 + /// `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, 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 { + /// 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, + /// 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, +} + // ─── Customization Enablement Union ─────────────────────────────────────── /// A single explicit customization enablement decision. @@ -4682,8 +5287,53 @@ 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), +} + +/// 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`), @@ -4699,5 +5349,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 05f2d96cd..7602a47ee 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -58,17 +58,17 @@ use ahp_types::actions::{ ChatTurnStartedAction, StateAction, }; use ahp_types::state::{ - ActiveTurn, AnnotationsState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, - ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, - CustomizationEnablement, 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, CustomizationEnablement, 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. @@ -1997,6 +1997,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::*; @@ -2023,6 +2133,7 @@ mod tests { title: String::new(), status: SessionStatus::Idle.bits(), activity: None, + origin: None, project: None, working_directories: None, annotations: None, @@ -2447,6 +2558,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 f439fcb23..91802b12e 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,176 @@ 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( + 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 + /// New or replacement run summary. + 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 + /// {@link AutomationRunSummary.resource} to remove. + 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 + /// 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( + 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 + /// Complete replacement lifecycle. + public var lifecycle: AutomationRunLifecycle + /// Complete replacement operation list. + 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 + /// Session URI to append when it is not already linked. + 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 + /// Linked session URI to remove. + 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 + /// New primary linked session, or omitted to clear the selection. + 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 + /// New or replacement artifact. + 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 + /// {@link AutomationRunArtifact.id} to remove. + 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 +2283,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 +2476,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 +2590,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..6f24c6982 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -266,6 +266,9 @@ 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-owned automation support. Absence means the host does not expose an + /// automation catalogue or automation commands. + public var automations: AutomationCapabilities? public init( protocolVersion: String, @@ -275,7 +278,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 +289,7 @@ public struct InitializeResult: Codable, Sendable { self.completionTriggerCharacters = completionTriggerCharacters self.terminalCommandPrefix = terminalCommandPrefix self.telemetry = telemetry + self.automations = automations } } @@ -309,6 +314,86 @@ 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( + 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 { + /// How long automatic trigger evaluation remains available. + public var lifetime: AutomationExecutionLifetime + + public init( + lifetime: AutomationExecutionLifetime + ) { + self.lifetime = lifetime + } +} + +public struct AutomationCreateCapability: Codable, Sendable { + + public init( + + ) { + } +} + +public struct AutomationScheduleCapabilities: Codable, Sendable { + /// Smallest permitted interval between consecutive occurrences. Omission + /// means no restriction beyond the cron format's one-minute resolution. + public var minIntervalMinutes: Int? + + public init( + minIntervalMinutes: Int? = nil + ) { + 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 +1905,389 @@ 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. + 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? + /// Optional exact filter on {@link AutomationDefinition.enabled}. + 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 + } +} + +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? + /// Automation summaries in host-defined catalogue order. + 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 + /// 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 { + 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 + } +} + +public struct ListAutomationTriggerDefinitionsResult: Codable, Sendable { + /// Available event trigger definitions. + 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 + /// 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 + /// 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 + case meta = "_meta" + case definition + case `import` = "import" + } + + public init( + channel: String, + meta: [String: AnyCodable]? = nil, + definition: AutomationDefinition, + `import`: AutomationImport? = nil + ) { + self.channel = channel + self.meta = meta + self.definition = definition + self.`import` = `import` + } +} + +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, + 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 + } +} + +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 { + 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 + /// 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 { + 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 + } +} + +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, + 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]? + /// 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 { + 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 + } +} + +public struct RunAutomationResult: Codable, Sendable { + /// Subscribable `ahp-automation-run:` URI. + 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 + /// 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 { + 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 + } +} + +public struct FetchAutomationRunsResult: Codable, Sendable { + + public init( + + ) { + } +} + +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]? + /// 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 { + 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 + } +} + +public struct PreviewAutomationScheduleResult: Codable, Sendable { + /// Ascending ISO 8601 timestamps. + 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 44147e2c6..120ff66f7 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -66,6 +66,51 @@ 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( + channel: String, + summary: AutomationSummary + ) { + self.channel = channel + self.summary = summary + } +} + +public struct AutomationRemovedParams: Codable, Sendable { + /// Root channel URI. + public var channel: String + /// Removed `ahp-automation:` URI. + public var automation: String + + public init( + channel: String, + automation: String + ) { + self.channel = channel + self.automation = automation + } +} + +public struct AutomationSummaryChangedParams: Codable, Sendable { + /// Root channel URI. + public var channel: String + /// Complete replacement catalogue summary. + 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 +225,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 +266,7 @@ public struct PartialSessionSummary: Codable, Sendable { case title case status case activity + case origin case project case workingDirectories case annotations @@ -234,6 +282,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 +296,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 2f61d18a0..ebce2cce4 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -407,6 +407,110 @@ 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" +} + +/// 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" +} + // MARK: - State Types public struct Icon: Codable, Sendable { @@ -1084,6 +1188,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 @@ -1182,6 +1288,7 @@ public struct SessionState: Codable, Sendable { case title case status case activity + case origin case project case workingDirectories case annotations @@ -1203,6 +1310,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, @@ -1222,6 +1330,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 @@ -1407,6 +1516,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 @@ -1446,6 +1557,7 @@ public struct SessionSummary: Codable, Sendable { case title case status case activity + case origin case project case workingDirectories case annotations @@ -1461,6 +1573,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, @@ -1474,6 +1587,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 @@ -5271,6 +5385,698 @@ 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( + kind: SessionOriginKind, + automation: String, + run: String + ) { + self.kind = kind + self.automation = automation + self.run = run + } +} + +public struct AutomationSchedule: Codable, Sendable { + /// Five-field AHP cron expression described by {@link AutomationSchedule}. + public var expression: String + /// IANA Time Zone Database identifier used to interpret the expression, for + /// example `"UTC"` or `"Europe/Berlin"`. + public var timeZone: String + + public init( + expression: String, + timeZone: String + ) { + self.expression = expression + self.timeZone = timeZone + } +} + +public struct AutomationScheduleTrigger: Codable, Sendable { + /// 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( + id: String, + kind: AutomationTriggerKind, + schedule: AutomationSchedule, + misfirePolicy: AutomationMisfirePolicy? = nil + ) { + self.id = id + self.kind = kind + self.schedule = schedule + self.misfirePolicy = misfirePolicy + } +} + +public struct AutomationEventTrigger: Codable, Sendable { + /// Identifier unique and stable within this automation definition. Run causes + /// refer back to this value. + public var id: String + public var kind: AutomationTriggerKind + /// Matches {@link AutomationTriggerDefinition.type}. + public var type: String + /// Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + /// trigger type. + public var events: [String] + /// Values described by {@link AutomationTriggerDefinition.configSchema}. + /// Clients MUST preserve unknown entries when editing other fields. + public var config: [String: AnyCodable]? + + 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 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( + id: String, + title: String, + description: String? = nil + ) { + self.id = id + self.title = title + self.description = description + } +} + +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( + 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 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? + /// Ordered working-directory URIs for each created session. Absence means a + /// workspace-less session. + public var workingDirectories: [String]? + /// Session configuration values accepted by `createSession`, normally + /// obtained from `resolveSessionConfig`. + public var config: [String: AnyCodable]? + + 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 + } +} + +public struct AutomationDefinition: Codable, Sendable { + /// Human-readable automation name. + public var title: String + /// 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 + /// Whether automatic triggers may create runs. Manual runs remain available + /// whenever {@link AutomationOperation.Run} is advertised. + public var enabled: Bool + /// 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 { + case title + case message + case session + case enabled + case triggers + case meta = "_meta" + } + + 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 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 { + 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 { + /// 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 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? + /// 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 { + 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 { + /// 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 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. + 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 { + 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 { + /// Category of the outstanding dependency. + 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 + /// 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, 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( + 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 + /// Run creation timestamp in ISO 8601 format. + 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 + /// 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( + status: AutomationRunStatus, + createdAt: String, + startedAt: String + ) { + self.status = status + self.createdAt = createdAt + self.startedAt = startedAt + } +} + +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( + 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 + /// 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( + 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 + /// 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( + 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 + /// 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( + 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? + /// 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 { + 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 { + /// 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 { + 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 { + /// 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 { + 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: - Customization Enablement Union /// A single explicit customization enablement decision. @@ -6087,6 +6893,156 @@ 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 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) @@ -6142,7 +7098,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) @@ -6151,6 +7107,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 @@ -6169,6 +7127,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)) } @@ -6183,6 +7145,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 9e46edc9e..423b67829 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/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..5faa92793 --- /dev/null +++ b/docs/guide/automations.md @@ -0,0 +1,491 @@ +# Automations + +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. + +## Key design points + +- **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 +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. 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. 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. +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. 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 + +`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 +} +``` + +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. + +## Multiple clients and reconciliation + +Several clients may subscribe to the same authority: + +- 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. + +The core invariant is: + +> Once an automation belongs to an AHP authority, clients never schedule or +> execute a fallback copy. + +This is what prevents duplicate runs across windows, devices, and +applications. + +## Security + +- 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 new file mode 100644 index 000000000..cd3f1a95a --- /dev/null +++ b/docs/specification/automation-channel.md @@ -0,0 +1,79 @@ +# 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 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 +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 +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. 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. + +## 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 d0731e6f5..cda79dae2 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -2090,6 +2090,216 @@ "changes" ] }, + "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", + "description": "Complete replacement definition." + }, + "revision": { + "type": "number", + "description": "New monotonic revision." + }, + "modifiedAt": { + "type": "string", + "description": "Definition modification timestamp in ISO 8601 format." + }, + "nextRunAt": { + "type": "string", + "description": "Earliest known future scheduled occurrence, or omitted to clear it." + } + }, + "required": [ + "type", + "definition", + "revision", + "modifiedAt" + ] + }, + "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", + "description": "New or replacement run summary." + } + }, + "required": [ + "type", + "run" + ] + }, + "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", + "description": "{@link AutomationRunSummary.resource} to remove." + } + }, + "required": [ + "type", + "run" + ] + }, + "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" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Older run summaries in newest-first order within this page." + }, + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next older page, or omitted at the end." + } + }, + "required": [ + "type", + "runs" + ] + }, + "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", + "description": "Complete replacement lifecycle." + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + }, + "description": "Complete replacement operation list." + } + }, + "required": [ + "type", + "lifecycle", + "operations" + ] + }, + "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", + "description": "Session URI to append when it is not already linked." + } + }, + "required": [ + "type", + "session" + ] + }, + "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", + "description": "Linked session URI to remove." + } + }, + "required": [ + "type", + "session" + ] + }, + "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", + "description": "New primary linked session, or omitted to clear the selection." + } + }, + "required": [ + "type" + ] + }, + "AutomationRunArtifactSetAction": { + "type": "object", + "description": "Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}.", + "properties": { + "type": { + "const": "automationRun/artifactSet" + }, + "artifact": { + "$ref": "#/$defs/AutomationRunArtifact", + "description": "New or replacement artifact." + } + }, + "required": [ + "type", + "artifact" + ] + }, + "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", + "description": "{@link AutomationRunArtifact.id} to remove." + } + }, + "required": [ + "type", + "artifactId" + ] + }, + "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" + } + }, + "required": [ + "type" + ] + }, "ChatToolCallConfirmedAction": { "oneOf": [ { @@ -2605,6 +2815,12 @@ }, { "$ref": "#/$defs/ChatState" + }, + { + "$ref": "#/$defs/AutomationState" + }, + { + "$ref": "#/$defs/AutomationRunState" } ], "description": "The current state of the resource" @@ -2832,6 +3048,28 @@ "values" ] }, + "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", + "description": "Owning `ahp-automation:` URI." + }, + "run": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` 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`.", @@ -2852,6 +3090,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" @@ -2894,6 +3136,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" @@ -3198,6 +3444,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" @@ -7092,87 +7342,809 @@ "type" ] }, - "StringOrMarkdown": { - "oneOf": [ - { - "type": "string" + "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": { + "expression": { + "type": "string", + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, - { - "type": "object", - "properties": { - "markdown": { - "type": "string" - } - }, - "required": [ - "markdown" - ] + "timeZone": { + "type": "string", + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } - ], - "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": [ + "expression", + "timeZone" + ] }, - "JsonPrimitive": { - "oneOf": [ - { - "type": "string" + "AutomationScheduleTrigger": { + "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", + "properties": { + "id": { + "type": "string", + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - { - "type": "number" + "kind": { + "const": "schedule" }, - { - "type": "boolean" + "schedule": { + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." }, - { - "type": "null" + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } - ], - "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "required": [ + "id", + "kind", + "schedule" + ] }, - "SessionInputRequest": { - "oneOf": [ - { - "$ref": "#/$defs/SessionChatInputRequest" + "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": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - { - "$ref": "#/$defs/SessionToolConfirmationRequest" + "kind": { + "const": "event" }, - { - "$ref": "#/$defs/SessionToolClientExecutionRequest" + "type": { + "type": "string", + "description": "Matches {@link AutomationTriggerDefinition.type}." }, - { - "$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." - }, - "CustomizationEnablement": { - "oneOf": [ - { - "type": "object", - "properties": { - "kind": { - "const": "global" - }, - "enabled": { - "type": "boolean" - } + "events": { + "type": "array", + "items": { + "type": "string" }, - "required": [ - "kind", - "enabled" - ] + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, - { + "config": { "type": "object", - "properties": { - "kind": { - "const": "workspace" - }, - "uri": { - "$ref": "#/$defs/URI" - }, - "enabled": { - "type": "boolean" + "additionalProperties": {}, + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." + } + }, + "required": [ + "id", + "kind", + "type", + "events" + ] + }, + "AutomationTriggerEventDefinition": { + "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", + "properties": { + "id": { + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." + }, + "title": { + "type": "string", + "description": "Human-readable label suitable for selection UI." + }, + "description": { + "type": "string", + "description": "Optional longer explanation of when this event fires." + } + }, + "required": [ + "id", + "title" + ] + }, + "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", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." + }, + "title": { + "type": "string", + "description": "Human-readable trigger type name." + }, + "description": { + "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", + "description": "Optional schema for {@link AutomationEventTrigger.config}." + } + }, + "required": [ + "type", + "title", + "events" + ] + }, + "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", + "description": "Provider id. Omit to use the host's default provider." + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "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", + "description": "Human-readable automation name." + }, + "message": { + "$ref": "#/$defs/Message", + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." + }, + "enabled": { + "type": "boolean", + "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": "Automatic triggers. An empty list means manual-only." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." + } + }, + "required": [ + "title", + "message", + "session", + "enabled", + "triggers" + ] + }, + "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": {}, + "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", + "description": "Subscribable `ahp-automation:` URI." + }, + "title": { + "type": "string", + "description": "Current {@link AutomationDefinition.title}." + }, + "enabled": { + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." + }, + "triggerCount": { + "type": "number", + "description": "Number of automatic triggers in the current definition." + }, + "nextRunAt": { + "type": "string", + "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", + "description": "Most recent retained run, when any run exists." + }, + "revision": { + "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", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "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", + "description": "URI of this automation channel." + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." + }, + "revision": { + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." + }, + "nextRunAt": { + "type": "string", + "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", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." + }, + "runsNextCursor": { + "type": "string", + "description": "Opaque cursor for the next older run-history page." + }, + "runtime": { + "$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", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." + } + }, + "required": [ + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationRunBlocker": { + "type": "object", + "description": "Summary of why a run cannot currently make progress.", + "properties": { + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "description": "Cause recorded for a client-requested manual run.", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", + "properties": { + "kind": { + "const": "trigger" + }, + "triggerId": { + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." + }, + "scheduledFor": { + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." + }, + "catchUp": { + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "description": "A durable run exists but has not begun external execution.", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "description": "The run is actively executing linked sessions.", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "description": "The run started but is temporarily unable to progress.", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "completedAt": { + "type": "string", + "description": "Completion timestamp in ISO 8601 format." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Failure timestamp in ISO 8601 format." + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "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", + "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", + "description": "Stable artifact id within this run, used by artifact actions." + }, + "label": { + "type": "string", + "description": "Human-readable label suitable for run-history UI." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "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", + "description": "Subscribable `ahp-automation-run:` URI." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." + }, + "primarySession": { + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." + }, + "sessionCount": { + "type": "number", + "description": "Number of linked sessions, including attempts and workers." + }, + "artifactCount": { + "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": {}, + "description": "Opaque host-defined summary metadata." + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "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", + "description": "URI of this automation-run channel." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$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", + "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": {}, + "description": "Opaque host-defined run metadata." + } + }, + "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", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." + }, + "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." + }, + "CustomizationEnablement": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "global" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "uri": { + "$ref": "#/$defs/URI" + }, + "enabled": { + "type": "boolean" } }, "required": [ @@ -7577,6 +8549,51 @@ ], "description": "A content part within terminal output." }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$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": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ], + "description": "Immutable provenance describing why a run was created." + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ], + "description": "Discriminated lifecycle of an automation run." + }, "StateAction": { "description": "Discriminated union of all state actions.", "oneOf": [ @@ -7837,6 +8854,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" } ] }, @@ -7910,6 +8960,13 @@ "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", + "description": "Operations the host currently permits for a run." + }, "PolicyState": { "enum": [ "enabled", @@ -7997,6 +9054,33 @@ ], "type": "string", "description": "Discriminant for {@link ResourceChange.type}." + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "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": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "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 e1911e570..5577baf8b 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-owned automation support. Absence means the host does not expose an\nautomation catalogue or automation commands." } }, "required": [ @@ -180,6 +184,77 @@ "snapshots" ] }, + "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", + "description": "Availability guarantee for automatic trigger execution." + }, + "create": { + "$ref": "#/$defs/AutomationCreateCapability", + "description": "Present when clients may call `createAutomation`." + }, + "schedules": { + "$ref": "#/$defs/AutomationScheduleCapabilities", + "description": "Present when definitions may contain schedule triggers." + }, + "runCancellation": { + "$ref": "#/$defs/AutomationRunCancellationCapability", + "description": "Present when clients may request cancellation on eligible runs." + }, + "schedulePreview": { + "$ref": "#/$defs/AutomationSchedulePreviewCapability", + "description": "Present when clients may call `previewAutomationSchedule`." + }, + "runHistoryLimit": { + "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": [ + "execution" + ] + }, + "AutomationExecutionCapabilities": { + "type": "object", + "description": "Automatic trigger execution availability.", + "properties": { + "lifetime": { + "$ref": "#/$defs/AutomationExecutionLifetime", + "description": "How long automatic trigger evaluation remains available." + } + }, + "required": [ + "lifetime" + ] + }, + "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": { + "minIntervalMinutes": { + "type": "number", + "description": "Smallest permitted interval between consecutive occurrences. Omission\nmeans no restriction beyond the cron format's one-minute resolution." + } + } + }, + "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": { "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,1213 +1573,1009 @@ "channel" ] }, - "Icon": { + "ListAutomationsParams": { "type": "object", - "description": "An optionally-sized icon that can be displayed in a user interface.", + "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": { - "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://" + ], + "description": "Automation catalogues are listed from the root channel." }, - "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." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "theme": { + "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", - "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", + "description": "Optional exact filter on {@link AutomationDefinition.enabled}." } }, "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.", + "description": "One page of the automation catalogue.", "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" + "$ref": "#/$defs/AutomationSummary" }, - "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`." + "description": "Automation summaries in host-defined catalogue order." } }, "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.", + "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": { - "type": { + "channel": { "type": "string", "enum": [ - "string", - "number", - "boolean", - "array", - "object" + "ahp-root://" ], - "description": "JSON Schema: property type" + "description": "Trigger definitions are discovered from the root channel." }, - "title": { - "type": "string", - "description": "JSON Schema: human-readable label for the property" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "description": { + "provider": { "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." - }, - "enumLabels": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Display extension: human-readable label per enum value (parallel array)" + "description": "Prospective provider id, or omitted for the host default." }, - "enumDescriptions": { + "workingDirectories": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/URI" }, - "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'`)" + "description": "Prospective ordered working-directory list." }, - "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": {}, + "description": "Prospective resolved session configuration values." } }, "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.", + "description": "Host-defined event trigger types available for the supplied context.", "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" + "$ref": "#/$defs/AutomationTriggerDefinition" }, - "description": "JSON Schema: list of required property ids" + "description": "Available event trigger definitions." } }, "required": [ - "type", - "properties" + "items" ] }, - "TextPosition": { + "AutomationImportTriggerNextRun": { "type": "object", - "description": "A zero-based position within a textual document.", + "description": "Initial schedule occurrence retained while an imported automation is disabled.", "properties": { - "line": { - "type": "number", - "description": "Zero-based line number." + "triggerId": { + "type": "string", + "description": "Stable id of a schedule trigger in the imported definition." }, - "character": { - "type": "number", - "description": "Zero-based character offset within the line." + "nextRunAt": { + "type": "string", + "description": "Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp." } }, "required": [ - "line", - "character" + "triggerId", + "nextRunAt" ] }, - "TextRange": { + "AutomationImport": { "type": "object", - "description": "A range within a textual document.", + "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": { - "start": { - "$ref": "#/$defs/TextPosition", - "description": "Start position of the range." + "source": { + "type": "string", + "description": "Stable namespace identifying the source implementation or store." }, - "end": { - "$ref": "#/$defs/TextPosition", - "description": "End position of the range." + "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." + }, + "triggerNextRuns": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationImportTriggerNextRun" + }, + "description": "Source schedule occurrences to retain until the imported definition is enabled." } }, "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" + "source", + "batchId", + "itemId" ] }, - "ContentRef": { + "CreateAutomationParams": { "type": "object", - "description": "A reference to large content stored outside the state tree.", + "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": { - "uri": { + "channel": { "$ref": "#/$defs/URI", - "description": "Content URI" + "description": "Client-chosen `ahp-automation:` URI for the new definition." }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "definition": { + "$ref": "#/$defs/AutomationDefinition", + "description": "Complete initial definition." }, - "nonce": { - "type": "string", - "description": "Content nonce" + "import": { + "$ref": "#/$defs/AutomationImport", + "description": "Optional legacy import state. When present, {@link definition} MUST be\ndisabled so automatic triggers cannot run before migration cutover." } }, "required": [ - "uri" + "channel", + "definition" ] }, - "FileEdit": { + "AutomationDefinitionPatch": { "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).", + "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": { - "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." + "title": { + "type": "string", + "description": "Replacement human-readable title." }, - "after": { - "type": "object", - "properties": { - "uri": { - "$ref": "#/$defs/URI" - }, - "content": { - "$ref": "#/$defs/ContentRef" - } + "message": { + "$ref": "#/$defs/Message", + "description": "Replacement initial user message." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Replacement session template." + }, + "enabled": { + "type": "boolean", + "description": "Replacement automatic-trigger enabled state." + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" }, - "required": [ - "uri", - "content" - ], - "description": "The file state after the edit. Absent for file deletions." + "description": "Complete replacement trigger list." }, - "diff": { + "_meta": { "type": "object", - "properties": { - "added": { - "type": "number" - }, - "removed": { - "type": "number" - } - }, - "description": "Optional diff display metadata" + "additionalProperties": {}, + "description": "Complete replacement implementation-defined metadata." } } }, - "UsageInfo": { + "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": { - "inputTokens": { - "type": "number", - "description": "Input tokens consumed" - }, - "outputTokens": { - "type": "number", - "description": "Output tokens generated" + "channel": { + "$ref": "#/$defs/URI", + "description": "Target `ahp-automation:` URI." }, - "model": { - "type": "string", - "description": "Model used" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "cacheReadTokens": { + "expectedRevision": { "type": "number", - "description": "Tokens read from cache" + "description": "Revision on which the client based {@link changes}." + }, + "changes": { + "$ref": "#/$defs/AutomationDefinitionPatch", + "description": "Editable fields to replace." + } + }, + "required": [ + "channel", + "expectedRevision", + "changes" + ] + }, + "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", + "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." + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." } - } + }, + "required": [ + "channel" + ] }, - "ErrorInfo": { + "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": { - "errorType": { - "type": "string", - "description": "Error type identifier" - }, - "message": { - "type": "string", - "description": "Human-readable error message" - }, - "stack": { - "type": "string", - "description": "Stack trace" + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." }, "_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)." + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, + "requestId": { + "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": [ - "errorType", - "message" + "channel", + "requestId" ] }, - "Snapshot": { + "RunAutomationResult": { "type": "object", - "description": "A point-in-time snapshot of a subscribed resource's state, returned by\n`initialize`, `reconnect`, and `subscribe`.", + "description": "Result identifying the existing or newly created run.", "properties": { - "resource": { + "run": { "$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`." + "description": "Subscribable `ahp-automation-run:` URI." } }, "required": [ - "resource", - "state", - "fromSeq" + "run" ] }, - "RootState": { + "FetchAutomationRunsParams": { "type": "object", - "description": "Global state shared with every client subscribed to `ahp-root://`.", + "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": { - "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" + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." }, "_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." + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, + "cursor": { + "type": "string", + "description": "Cursor previously received as {@link AutomationState.runsNextCursor}.\nOmit to request the first page not already included by the snapshot." } }, "required": [ - "agents" + "channel" ] }, - "AgentInfo": { + "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": { - "provider": { + "channel": { "type": "string", - "description": "Agent provider ID (e.g. `'copilot'`)" + "enum": [ + "ahp-root://" + ], + "description": "Schedule preview is requested from the root channel." }, - "displayName": { - "type": "string", - "description": "Human-readable name" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "description": { - "type": "string", - "description": "Description string" + "schedule": { + "$ref": "#/$defs/AutomationSchedule", + "description": "Portable AHP cron schedule to evaluate." }, - "models": { + "count": { + "type": "number", + "description": "Requested maximum number of future occurrences; the host MAY cap it." + } + }, + "required": [ + "channel", + "schedule" + ] + }, + "PreviewAutomationScheduleResult": { + "type": "object", + "description": "Host-canonical future schedule occurrences.", + "properties": { + "items": { "type": "array", "items": { - "$ref": "#/$defs/SessionModelInfo" + "type": "string" }, - "description": "Available models for this agent" + "description": "Ascending ISO 8601 timestamps." + } + }, + "required": [ + "items" + ] + }, + "Icon": { + "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." }, - "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." + "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\"`." }, - "customizations": { + "sizes": { "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 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." }, - "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." + "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": [ - "provider", - "displayName", - "description", - "models" + "src" ] }, - "AgentCapabilities": { + "ProtectedResourceMetadata": { "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.", + "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": { - "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": { + "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\"`)." }, - "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_name": { + "type": "string", + "description": "OPTIONAL. Human-readable name of the protected resource." }, - "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": { + "authorization_servers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OPTIONAL. JSON array of OAuth authorization server identifier URLs." + }, + "jwks_uri": { + "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." + }, + "resource_signing_alg_values_supported": { + "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": "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", - "values" + "line", + "character" ] }, - "SessionMetadata": { + "TextRange": { "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 range within a textual document.", "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" - }, - "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." + "start": { + "$ref": "#/$defs/TextPosition", + "description": "Start position of the range." }, - "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." + "end": { + "$ref": "#/$defs/TextPosition", + "description": "End position of the range." } }, "required": [ - "provider", - "title", - "status" + "start", + "end" ] }, - "SessionState": { + "TextSelection": { "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": "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": { - "provider": { - "type": "string", - "description": "Agent provider ID" - }, - "title": { - "type": "string", - "description": "Session title" + "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" }, - "status": { - "$ref": "#/$defs/SessionStatus", - "description": "Current session status" + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" }, - "activity": { + "contentType": { "type": "string", - "description": "Human-readable description of what the session is currently doing" - }, - "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" + "description": "Content MIME type" }, - "serverTools": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolDefinition" + "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" + } }, - "description": "Tools provided by the server (agent host) for this session" + "required": [ + "uri", + "content" + ], + "description": "The file state before the edit. Absent for file creations or for in-place file edits." }, - "activeClients": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionActiveClient" + "after": { + "type": "object", + "properties": { + "uri": { + "$ref": "#/$defs/URI" + }, + "content": { + "$ref": "#/$defs/ContentRef" + } }, - "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." + "required": [ + "uri", + "content" + ], + "description": "The file state after the edit. Absent for file deletions." }, - "chats": { - "type": "array", - "items": { - "$ref": "#/$defs/ChatSummary" + "diff": { + "type": "object", + "properties": { + "added": { + "type": "number" + }, + "removed": { + "type": "number" + } }, - "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" + "description": "Optional diff display metadata" + } + } + }, + "UsageInfo": { + "type": "object", + "properties": { + "inputTokens": { + "type": "number", + "description": "Input tokens consumed" }, - "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." + "outputTokens": { + "type": "number", + "description": "Output tokens generated" }, - "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." + "model": { + "type": "string", + "description": "Model used" }, - "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." + "cacheReadTokens": { + "type": "number", + "description": "Tokens read from cache" }, "_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 for this usage report.\nClients MAY look for well-known optional keys here to provide enhanced UI." } - }, - "required": [ - "provider", - "title", - "status", - "lifecycle", - "activeClients", - "chats" - ] + } }, - "SessionActiveClient": { + "ErrorInfo": { "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.", "properties": { - "clientId": { + "errorType": { "type": "string", - "description": "Client identifier (matches `clientId` from `initialize`)" + "description": "Error type identifier" }, - "displayName": { + "message": { "type": "string", - "description": "Human-readable client name (e.g. `\"VS Code\"`)" + "description": "Human-readable error message" }, - "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": { + "stack": { "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": "Stack trace" }, - "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." + "_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": [ - "id", - "chat" + "errorType", + "message" ] }, - "SessionChatInputRequest": { + "Snapshot": { "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": "A point-in-time snapshot of a subscribed resource's state, returned by\n`initialize`, `reconnect`, and `subscribe`.", "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": { + "resource": { "$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": "The subscribed channel URI (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)" }, - "kind": { - "const": "chatInput" + "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" }, - "request": { - "$ref": "#/$defs/ChatInputRequest", - "description": "The mirrored chat input request." + "fromSeq": { + "type": "number", + "description": "The `serverSeq` at which this snapshot was taken. Subsequent actions will have `serverSeq > fromSeq`." } }, "required": [ - "id", - "chat", - "kind", - "request" + "resource", + "state", + "fromSeq" ] }, - "SessionToolConfirmationRequest": { + "RootState": { "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": "Global state shared with every client subscribed to `ahp-root://`.", "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." + "agents": { + "type": "array", + "items": { + "$ref": "#/$defs/AgentInfo" + }, + "description": "Available agent backends and their models" }, - "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." + "activeSessions": { + "type": "number", + "description": "Number of active (non-disposed) sessions on the server" }, - "kind": { - "const": "toolConfirmation" + "terminals": { + "type": "array", + "items": { + "$ref": "#/$defs/TerminalInfo" + }, + "description": "Known terminals on the server. Subscribe to individual terminal URIs for full state." }, - "turnId": { - "type": "string", - "description": "The turn the tool call belongs to." + "config": { + "$ref": "#/$defs/RootConfigState", + "description": "Agent host configuration schema and current values" }, - "toolCall": { - "$ref": "#/$defs/ToolCallConfirmationState", - "description": "The tool call awaiting confirmation." + "_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": [ - "id", - "chat", - "kind", - "turnId", - "toolCall" + "agents" ] }, - "SessionToolClientExecutionRequest": { + "AgentInfo": { "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": { + "provider": { "type": "string", - "description": "The turn the tool call belongs to." + "description": "Agent provider ID (e.g. `'copilot'`)" }, - "clientId": { + "displayName": { "type": "string", - "description": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." + "description": "Human-readable name" }, - "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": { + "description": { "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": "Description string" }, - "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." + "models": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionModelInfo" + }, + "description": "Available models for this agent" }, - "kind": { - "const": "toolAuthentication" + "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." }, - "turnId": { - "type": "string", - "description": "The turn the tool call belongs to." + "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." }, - "toolCall": { - "$ref": "#/$defs/ToolCallAuthRequiredState", - "description": "The tool call awaiting authentication." + "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": [ - "id", - "chat", - "kind", - "turnId", - "toolCall" + "provider", + "displayName", + "description", + "models" ] }, - "ProjectInfo": { + "AgentCapabilities": { "type": "object", - "description": "Server-owned project metadata for a session.", + "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": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Project URI" + "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." }, - "displayName": { - "type": "string", - "description": "Human-readable project name" + "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}." } - }, - "required": [ - "uri", - "displayName" - ] + } }, - "SessionSummary": { + "MultipleChatsCapability": { "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": "Options for the {@link AgentCapabilities.multipleChats} capability.", "properties": { - "provider": { - "type": "string", - "description": "Agent provider ID" + "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." }, - "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" - }, - "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." - }, - "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", - "resource", - "createdAt", - "modifiedAt" - ] - }, - "ChangesSummary": { - "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.", - "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." - }, - "files": { - "type": "number", - "description": "Number of files that have changes." + "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." } } }, - "AgentSelection": { + "MultipleWorkingDirectoriesCapability": { "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.", + "description": "Options for the {@link AgentCapabilities.multipleWorkingDirectories} capability.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Stable agent URI (matches an {@link AgentCustomization.uri})." + "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." } - }, - "required": [ - "uri" - ] + } }, - "SessionConfigPropertySchema": { + "SessionModelInfo": { "type": "object", - "description": "A session configuration property descriptor.\n\nExtends the generic {@link ConfigPropertySchema} with session-specific\ndisplay extensions.", "properties": { - "type": { + "id": { "type": "string", - "enum": [ - "string", - "number", - "boolean", - "array", - "object" - ], - "description": "JSON Schema: property type" + "description": "Model identifier" }, - "title": { + "provider": { "type": "string", - "description": "JSON Schema: human-readable label for the property" + "description": "Provider this model belongs to" }, - "description": { + "name": { "type": "string", - "description": "JSON Schema: description / tooltip" - }, - "default": { - "description": "JSON Schema: default value" + "description": "Human-readable model name" }, - "enum": { - "type": "array", - "items": { - "$ref": "#/$defs/JsonPrimitive" - }, - "description": "JSON Schema: allowed values. May be primitives of any JSON type." + "maxContextWindow": { + "type": "number", + "description": "Maximum context window size" }, - "enumLabels": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Display extension: human-readable label per enum value (parallel array)" + "maxOutputTokens": { + "type": "number", + "description": "Maximum number of output tokens the model can generate" }, - "enumDescriptions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Display extension: description per enum value (parallel array)" + "maxPromptTokens": { + "type": "number", + "description": "Maximum number of prompt (input) tokens the model accepts" }, - "readOnly": { + "supportsVision": { "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": { - "type": "string" - }, - "description": "JSON Schema: list of required property ids (used when `type` is `'object'`)" + "description": "Whether the model supports vision" }, - "additionalProperties": { - "$ref": "#/$defs/ConfigPropertySchema", - "description": "JSON Schema: schema for additional properties not listed in `properties` (used when `type` is `'object'`)." + "policyState": { + "$ref": "#/$defs/PolicyState", + "description": "Policy configuration state" }, - "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." + "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." }, - "sessionMutable": { - "type": "boolean", - "description": "When `true`, the user may change this property after session creation" + "_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": [ - "type", - "title" + "id", + "provider", + "name" ] }, - "SessionConfigSchema": { + "ModelSelection": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.", + "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": { - "type": { + "id": { "type": "string", - "enum": [ - "object" - ], - "description": "JSON Schema: always `'object'`" + "description": "Model identifier" }, - "properties": { + "config": { "type": "object", "additionalProperties": { - "$ref": "#/$defs/SessionConfigPropertySchema" - }, - "description": "JSON Schema: property descriptors keyed by property id" - }, - "required": { - "type": "array", - "items": { - "type": "string" + "$ref": "#/$defs/JsonPrimitive" }, - "description": "JSON Schema: list of required property ids" + "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": [ - "type", - "properties" + "id" ] }, - "SessionConfigState": { + "RootConfigState": { "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.", + "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/SessionConfigSchema", + "$ref": "#/$defs/ConfigSchema", "description": "JSON Schema describing available configuration properties" }, "values": { @@ -2718,608 +2589,733 @@ "values" ] }, - "ToolDefinition": { + "AutomationSessionOrigin": { "type": "object", - "description": "Describes a tool available in a session, provided by either the server or the active client.", + "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": { - "name": { + "kind": { + "const": "automation" + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "run": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` 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": "Unique tool identifier" + "description": "Agent provider ID" }, "title": { "type": "string", - "description": "Human-readable display name" + "description": "Session title" }, - "description": { + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current session status" + }, + "activity": { "type": "string", - "description": "Description of what the tool does" + "description": "Human-readable description of what the session is currently doing" }, - "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`." + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." }, - "outputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "object" - ] - }, - "properties": { - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } + "project": { + "$ref": "#/$defs/ProjectInfo", + "description": "Server-owned project for this session" + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" }, - "required": [ - "type" - ], - "description": "JSON Schema defining the structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." + "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/ToolAnnotations", - "description": "Behavioral hints about the tool. All properties are advisory." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." + "$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": [ - "name" + "provider", + "title", + "status" ] }, - "ToolAnnotations": { + "SessionState": { "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": "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": "Alternate human-readable title" + "description": "Session title" }, - "readOnlyHint": { - "type": "boolean", - "description": "Tool does not modify its environment (default: false)" + "status": { + "$ref": "#/$defs/SessionStatus", + "description": "Current session status" }, - "destructiveHint": { - "type": "boolean", - "description": "Tool may perform destructive updates (default: true)" + "activity": { + "type": "string", + "description": "Human-readable description of what the session is currently doing" }, - "idempotentHint": { - "type": "boolean", - "description": "Repeated calls with the same arguments have no additional effect (default: false)" + "origin": { + "$ref": "#/$defs/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." }, - "openWorldHint": { - "type": "boolean", - "description": "Tool may interact with external entities (default: true)" - } - } - }, - "CustomizationBase": { - "type": "object", - "description": "Fields shared by every customization variant.", - "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)." + "project": { + "$ref": "#/$defs/ProjectInfo", + "description": "Server-owned project for this session" }, - "uri": { + "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": "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": "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." }, - "name": { - "type": "string", - "description": "Human-readable name." + "config": { + "$ref": "#/$defs/SessionConfigState", + "description": "Session configuration schema and current values" }, - "icons": { + "customizations": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/Customization" }, - "description": "Icons for UI display." + "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." }, - "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." + "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 customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." + "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": [ - "id", - "uri", - "name" + "provider", + "title", + "status", + "lifecycle", + "activeClients", + "chats" ] }, - "CustomizationLoadingState": { + "SessionActiveClient": { "type": "object", - "description": "Container is being loaded by the host.", + "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": { - "kind": { - "const": "loading" + "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": [ - "kind" + "clientId", + "tools" ] }, - "CustomizationLoadedState": { + "SessionInputRequestBase": { "type": "object", - "description": "Container loaded successfully.", + "description": "Fields common to every {@link SessionInputRequest} variant.", "properties": { - "kind": { - "const": "loaded" + "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": [ - "kind" + "id", + "chat" ] }, - "CustomizationDegradedState": { + "SessionChatInputRequest": { "type": "object", - "description": "Container partially loaded but has warnings.", + "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": "degraded" + "const": "chatInput" }, - "message": { - "type": "string", - "description": "Human-readable description of the warning." + "request": { + "$ref": "#/$defs/ChatInputRequest", + "description": "The mirrored chat input request." } }, "required": [ + "id", + "chat", "kind", - "message" + "request" ] }, - "CustomizationErrorState": { + "SessionToolConfirmationRequest": { "type": "object", - "description": "Container failed to load.", + "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": "error" + "const": "toolConfirmation" }, - "message": { + "turnId": { "type": "string", - "description": "Human-readable error message." + "description": "The turn the tool call belongs to." + }, + "toolCall": { + "$ref": "#/$defs/ToolCallConfirmationState", + "description": "The tool call awaiting confirmation." } }, "required": [ + "id", + "chat", "kind", - "message" + "turnId", + "toolCall" ] }, - "ContainerCustomizationBase": { + "SessionToolClientExecutionRequest": { "type": "object", - "description": "Fields shared by container customizations.", + "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": "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": "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." }, - "uri": { + "chat": { "$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." + "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." }, - "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." + "kind": { + "const": "toolClientExecution" }, - "_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." + "turnId": { + "type": "string", + "description": "The turn the tool call belongs to." }, "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": "The `clientId` expected to execute the tool. Matches the `clientId` of the\ntool call's client {@link ToolCallContributor}." }, - "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." + "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", - "uri", - "name" + "chat", + "kind", + "turnId", + "clientId", + "toolCall" ] }, - "PluginCustomization": { + "SessionToolAuthenticationRequest": { "type": "object", - "description": "An [Open Plugins](https://open-plugins.com/) plugin.", + "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": "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": "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." }, - "uri": { + "chat": { "$ref": "#/$defs/URI", - "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." - }, - "name": { - "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + "description": "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." }, - "_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." + "kind": { + "const": "toolAuthentication" }, - "clientId": { + "turnId": { "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" - }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + "description": "The turn the tool call belongs to." }, - "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." + "toolCall": { + "$ref": "#/$defs/ToolCallAuthRequiredState", + "description": "The tool call awaiting authentication." } }, "required": [ "id", - "uri", - "name", - "type" + "chat", + "kind", + "turnId", + "toolCall" ] }, - "ClientPluginCustomization": { + "ProjectInfo": { "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": "Server-owned project metadata for a session.", "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": "Project URI" }, - "name": { + "displayName": { "type": "string", - "description": "Human-readable name." - }, - "icons": { - "type": "array", - "items": { - "$ref": "#/$defs/Icon" - }, - "description": "Icons for UI display." + "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", + "description": "Agent provider ID" }, - "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." + "title": { + "type": "string", + "description": "Session 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 session status" }, - "clientId": { + "activity": { "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "description": "Human-readable description of what the session is currently doing" }, - "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/SessionOrigin", + "description": "Durable origin of this session, when another AHP resource created it." }, - "children": { + "project": { + "$ref": "#/$defs/ProjectInfo", + "description": "Server-owned project for this session" + }, + "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." + "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." }, - "type": { - "const": "plugin" + "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." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + "resource": { + "$ref": "#/$defs/URI", + "description": "Session URI" }, - "version": { + "createdAt": { "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": "Creation timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "nonce": { + "modifiedAt": { "type": "string", - "description": "Opaque version token used by the host to detect changes." + "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" }, - "childEnablement": { + "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": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - } - }, - "description": "Explicit enablement decisions for children this plugin contributes,\nkeyed by child name (for MCP servers, the server name as it appears in\nthe bundled `.mcp.json`).\n\nBundled children are discovered by the host rather than published by the\nclient, so the client cannot attach `enablement` to them directly. This\ncarries the client's global decision for each one; the host applies it\nunder the child's durable key." + "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": [ - "id", - "uri", - "name", - "type" + "provider", + "title", + "status", + "resource", + "createdAt", + "modifiedAt" ] }, - "DirectoryCustomization": { + "ChangesSummary": { "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": "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": { - "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)." + "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": "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 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", + "enum": [ + "string", + "number", + "boolean", + "array", + "object" + ], + "description": "JSON Schema: property type" }, - "name": { + "title": { "type": "string", - "description": "Human-readable name." + "description": "JSON Schema: human-readable label for the property" }, - "icons": { + "description": { + "type": "string", + "description": "JSON Schema: description / tooltip" + }, + "default": { + "description": "JSON Schema: default value" + }, + "enum": { "type": "array", "items": { - "$ref": "#/$defs/Icon" + "$ref": "#/$defs/JsonPrimitive" }, - "description": "Icons for UI display." + "description": "JSON Schema: allowed values. May be primitives of any JSON type." }, - "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." + "enumLabels": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Display extension: human-readable label 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." + "enumDescriptions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Display extension: description per enum value (parallel array)" }, - "clientId": { - "type": "string", - "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." + "readOnly": { + "type": "boolean", + "description": "JSON Schema: when `true`, the property is displayed but cannot be modified by the user" }, - "load": { - "$ref": "#/$defs/CustomizationLoadState", - "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." + "items": { + "$ref": "#/$defs/ConfigPropertySchema", + "description": "JSON Schema: schema for array items (used when `type` is `'array'`)" }, - "children": { + "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/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." + "description": "JSON Schema: list of required property ids (used when `type` is `'object'`)" }, - "type": { - "const": "directory" + "additionalProperties": { + "$ref": "#/$defs/ConfigPropertySchema", + "description": "JSON Schema: schema for additional properties not listed in `properties` (used when `type` is `'object'`)." }, - "enabled": { + "enumDynamic": { "type": "boolean", - "description": "Whether this container is currently enabled." - }, - "contents": { - "$ref": "#/$defs/ChildCustomizationType", - "description": "Which child customization type this directory holds." + "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." }, - "writable": { + "sessionMutable": { "type": "boolean", - "description": "Whether clients may write into this directory." + "description": "When `true`, the user may change this property after session creation" } }, "required": [ - "id", - "uri", - "name", "type", - "enabled", - "contents", - "writable" + "title" ] }, - "ChildCustomizationBase": { + "SessionConfigSchema": { "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 because it can appear as a top-level customization too.", + "description": "A JSON Schema object describing available session configuration metadata.", "properties": { - "id": { + "type": { "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." + "enum": [ + "object" + ], + "description": "JSON Schema: always `'object'`" }, - "name": { - "type": "string", - "description": "Human-readable name." + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/SessionConfigPropertySchema" + }, + "description": "JSON Schema: property descriptors keyed by property id" }, - "icons": { + "required": { "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." + "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" }, - "_meta": { + "values": { "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + "description": "Current configuration values" } }, "required": [ - "id", - "uri", - "name" + "schema", + "values" ] }, - "AgentCustomization": { + "ToolDefinition": { "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": "Describes a tool available in a session, provided by either the server or the active client.", "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." - }, - "type": { - "const": "agent" + "description": "Unique tool identifier" }, - "description": { + "title": { "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 display name" }, - "model": { + "description": { "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": "Description of what the tool does" }, - "tools": { - "type": "array", - "items": { - "type": "string" + "inputSchema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "object" + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "required": { + "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." + "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`." }, - "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." + "outputSchema": { + "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 structure of the tool's output.\n\nMirrors MCP `Tool.outputSchema`." }, - "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." + "annotations": { + "$ref": "#/$defs/ToolAnnotations", + "description": "Behavioral hints about the tool. All properties are advisory." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata.\n\nMirrors the MCP `_meta` convention." } }, "required": [ - "id", - "uri", - "name", - "type" + "name" ] }, - "SkillCustomization": { + "ToolAnnotations": { "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": "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": { + "title": { + "type": "string", + "description": "Alternate human-readable title" + }, + "readOnlyHint": { + "type": "boolean", + "description": "Tool does not modify its environment (default: false)" + }, + "destructiveHint": { + "type": "boolean", + "description": "Tool may perform destructive updates (default: true)" + }, + "idempotentHint": { + "type": "boolean", + "description": "Repeated calls with the same arguments have no additional effect (default: false)" + }, + "openWorldHint": { + "type": "boolean", + "description": "Tool may interact with external entities (default: true)" + } + } + }, + "CustomizationBase": { + "type": "object", + "description": "Fields shared by every customization variant.", "properties": { "id": { "type": "string", @@ -3348,37 +3344,75 @@ "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", + "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" + ] + }, + "CustomizationDegradedState": { + "type": "object", + "description": "Container partially loaded but has warnings.", + "properties": { + "kind": { + "const": "degraded" }, - "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." - }, - "type": { - "const": "skill" - }, - "description": { + "message": { "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." + "description": "Human-readable description of the warning." + } + }, + "required": [ + "kind", + "message" + ] + }, + "CustomizationErrorState": { + "type": "object", + "description": "Container failed to load.", + "properties": { + "kind": { + "const": "error" }, - "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." + "message": { + "type": "string", + "description": "Human-readable error message." } }, "required": [ - "id", - "uri", - "name", - "type" + "kind", + "message" ] }, - "PromptCustomization": { + "ContainerCustomizationBase": { "type": "object", - "description": "A prompt contributed by a plugin or directory.", + "description": "Fields shared by container customizations.", "properties": { "id": { "type": "string", @@ -3408,28 +3442,31 @@ "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "type": { - "const": "prompt" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "description": { - "type": "string", - "description": "Short description of what the prompt does." + "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", - "name", - "type" + "name" ] }, - "RuleCustomization": { + "PluginCustomization": { "type": "object", - "description": "A rule contributed by a plugin or directory.\n\nMirrors the [Open Plugins rule](https://open-plugins.com/agent-builders/components/rules)\nformat: a markdown file (e.g. `.mdc`) whose body is injected into\ncontext while the rule is active. This type also covers tool-specific\n\"instruction\" formats (e.g. VS Code Copilot's\n`.github/instructions/*.md`), which differ only in naming — they\nshare the same semantics of `description`, optional always-on\nactivation, and optional glob scoping.", + "description": "An [Open Plugins](https://open-plugins.com/) plugin.", "properties": { "id": { "type": "string", @@ -3459,27 +3496,34 @@ "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "type": { - "const": "rule" + "load": { + "$ref": "#/$defs/CustomizationLoadState", + "description": "Host-reported load state. Absent means the host has not yet reported\na load state for this container." }, - "description": { - "type": "string", - "description": "Description of what the rule enforces." + "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." }, - "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." + "type": { + "const": "plugin" }, - "globs": { + "enablement": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/CustomizationEnablement" }, - "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, + "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": [ @@ -3489,9 +3533,9 @@ "type" ] }, - "HookCustomization": { + "ClientPluginCustomization": { "type": "object", - "description": "A hook manifest contributed by a plugin or directory.", + "description": "A {@link PluginCustomization} as published by a client. Extends the\nserver-facing shape with an opaque `nonce` so the host can detect when\nthe client's view of a plugin has changed and re-parse only as needed.\n\nClients SHOULD include a `nonce`. Server-side fields like\n{@link ContainerCustomizationBase.children | `children`} and\n{@link ContainerCustomizationBase.load | `load`} are typically left\nabsent on publication and populated by the host when the resolved\nplugin appears in {@link SessionState.customizations}.", "properties": { "id": { "type": "string", @@ -3521,12 +3565,48 @@ "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + "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": "hook" + "const": "plugin" + }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, + "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." + }, + "childEnablement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + } + }, + "description": "Explicit enablement decisions for children this plugin contributes,\nkeyed by child name (for MCP servers, the server name as it appears in\nthe bundled `.mcp.json`).\n\nBundled children are discovered by the host rather than published by the\nclient, so the client cannot attach `enablement` to them directly. This\ncarries the client's global decision for each one; the host applies it\nunder the child's durable key." } }, "required": [ @@ -3536,9 +3616,9 @@ "type" ] }, - "McpServerCustomization": { + "DirectoryCustomization": { "type": "object", - "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", + "description": "A directory the host watches for this session.\n\nPresence in the customization list signals that the host may discover\ncustomizations from this directory. When `writable` is `true`, clients\nMAY persist new customizations into the directory using\n[`resourceWrite`](/reference/common#resourcewrite); the host will\nthen surface the resulting child via the customization actions.\n\nThe directory may not yet exist on disk.", "properties": { "id": { "type": "string", @@ -3568,31 +3648,35 @@ "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" + "clientId": { + "type": "string", + "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." }, - "enablement": { + "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/CustomizationEnablement" + "$ref": "#/$defs/ChildCustomization" }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nFlows in both directions. A client publishes this alongside a customization\nto assert its global decision, which is authoritative for the Global scope;\na client always includes its global entry, even when enabled. The host\npublishes the fully resolved set across all scopes, and consumers derive\nthe effective enabled value from that set." + "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." }, - "isClientBundled": { - "type": "boolean", - "description": "Whether the client explicitly bundled this server and owns its Global\nenablement decision." + "type": { + "const": "directory" }, - "state": { - "$ref": "#/$defs/McpServerState", - "description": "Current lifecycle state of the MCP server." + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." }, - "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." + "contents": { + "$ref": "#/$defs/ChildCustomizationType", + "description": "Which child customization type this directory holds." }, - "mcpApp": { - "$ref": "#/$defs/McpServerCustomizationApps", - "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." + "writable": { + "type": "boolean", + "description": "Whether clients may write into this directory." } }, "required": [ @@ -3600,1435 +3684,2313 @@ "uri", "name", "type", - "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" + "enabled", + "contents", + "writable" ] }, - "AhpMcpUiHostCapabilities": { + "ChildCustomizationBase": { "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": "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 because it can appear as a top-level customization too.", "properties": { - "serverTools": { - "type": "object", - "properties": { - "listChanged": { - "type": "boolean" - } - }, - "description": "Producer proxies the MCP `tools/*` methods to the upstream server." + "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)." }, - "serverResources": { - "type": "object", - "properties": { - "listChanged": { - "type": "boolean" - } + "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": "Producer proxies the MCP `resources/*` methods to the upstream server." + "description": "Icons for UI display." }, - "logging": { + "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": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." + "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." }, - "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" + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." } }, "required": [ - "kind" + "id", + "uri", + "name" ] }, - "McpOAuthClient": { + "AgentCustomization": { "type": "object", - "description": "A pre-registered OAuth client that clients use instead of dynamic client\nregistration when resolving an MCP authentication challenge.", + "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": { - "clientId": { + "id": { "type": "string", - "description": "OAuth client identifier registered with the authorization server." + "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)." }, - "clientSecret": { + "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": "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": [ - "clientId" - ] - }, - "McpAuthRequirement": { - "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.", - "properties": { - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "description": "Human-readable name." }, - "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." + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "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." + "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." }, - "requiredScopes": { + "_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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its 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": "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": "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." }, - "description": { - "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." + "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": [ - "reason", - "resource" + "id", + "uri", + "name", + "type" ] }, - "McpServerAuthRequiredState": { + "SkillCustomization": { "type": "object", - "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", + "description": "A 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": { - "reason": { - "$ref": "#/$defs/McpAuthRequiredReason", - "description": "Why authentication is required." + "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)." }, - "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." + "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." }, - "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." + "name": { + "type": "string", + "description": "Human-readable name." }, - "requiredScopes": { + "icons": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/Icon" }, - "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": "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, + "type": { + "const": "skill" }, "description": { "type": "string", - "description": "Human-readable hint, typically from the OAuth `error_description`." + "description": "Short description used for help text and auto-invocation matching.\nSourced from the skill's frontmatter `description`." }, - "kind": { - "const": "authRequired" - } - }, - "required": [ - "reason", - "resource", - "kind" - ] - }, - "McpServerErrorState": { - "type": "object", - "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", - "properties": { - "kind": { - "const": "error" + "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." }, - "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" + "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." } }, "required": [ - "kind" + "id", + "uri", + "name", + "type" ] }, - "ChatState": { + "PromptCustomization": { "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.", + "description": "A prompt contributed by a plugin or directory.", "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": { + "id": { "type": "string", - "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" - }, - "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": { - "$ref": "#/$defs/URI" - }, - "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": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "turns": { - "type": "array", - "items": { - "$ref": "#/$defs/Turn" - }, - "description": "Completed turns" + "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." }, - "turnsNextCursor": { + "name": { "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." - }, - "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" + "description": "Human-readable name." }, - "queuedMessages": { + "icons": { "type": "array", "items": { - "$ref": "#/$defs/PendingMessage" + "$ref": "#/$defs/Icon" }, - "description": "Messages to send automatically as new turns after the current turn finishes" + "description": "Icons for UI display." }, - "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." + "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 chat." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its 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": [ - "resource", - "title", - "status", - "modifiedAt", - "turns" + "id", + "uri", + "name", + "type" ] }, - "ChatSummary": { + "RuleCustomization": { "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 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": { - "resource": { + "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": "Chat 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." }, - "title": { + "name": { "type": "string", - "description": "Chat title" + "description": "Human-readable name." }, - "status": { - "$ref": "#/$defs/SessionStatus", - "description": "Current chat status (reuses SessionStatus shape)" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "activity": { - "type": "string", - "description": "Human-readable description of what the chat is currently doing" + "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." }, - "modifiedAt": { - "type": "string", - "description": "Last modification timestamp (ISO 8601, e.g. `\"2025-03-10T18:42:03.123Z\"`)" + "_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." }, - "origin": { - "$ref": "#/$defs/ChatOrigin", - "description": "How this chat came into existence" + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, - "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." + "type": { + "const": "rule" }, - "workingDirectories": { + "description": { + "type": "string", + "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": { - "$ref": "#/$defs/URI" + "type": "string" }, - "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." - } - }, - "required": [ - "resource", - "title", - "status", - "modifiedAt" - ] - }, - "SideChatSelection": { - "type": "object", - "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", - "properties": { - "text": { - "type": "string", - "description": "Exact selected-text snapshot captured at `createChat` acceptance.\n\nMUST be non-empty." - }, - "responsePartId": { - "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`." - } - }, - "required": [ - "text" - ] - }, - "PendingMessage": { - "type": "object", - "description": "A message queued for future delivery to the agent.\n\nSteering messages are injected into the current turn mid-flight.\nQueued messages are automatically started as new turns after the\ncurrent turn naturally finishes.", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this pending message" - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that will start the next turn" + "description": "Glob patterns the rule applies to. When present, the rule is only\nactive for matching files." } }, "required": [ "id", - "message" + "uri", + "name", + "type" ] }, - "ChatInputOption": { + "HookCustomization": { "type": "object", - "description": "A choice in a select-style question.", + "description": "A hook manifest contributed by a plugin or directory.", "properties": { "id": { "type": "string", - "description": "Stable option identifier; for MCP enum values this is the enum 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)." }, - "label": { - "type": "string", - "description": "Display label" + "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": "Optional secondary text" + "description": "Human-readable name." }, - "recommended": { - "type": "boolean", - "description": "Whether this option is the recommended/default choice" - } - }, - "required": [ - "id", - "label" - ] - }, - "ChatInputQuestionBase": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "title": { - "type": "string", - "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": { + "enabled": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, + "type": { + "const": "hook" } }, "required": [ "id", - "message" + "uri", + "name", + "type" ] }, - "ChatInputTextQuestion": { + "McpServerCustomization": { "type": "object", - "description": "Text question within a chat input request.", + "description": "An MCP server contributed by a plugin or directory.\n\nWhen the server is declared inline in the containing plugin manifest,\n`uri` points at the manifest file and\n{@link CustomizationBase.range | `range`} narrows it to the\ndeclaration's span.\n\nThe MCP server customization also reflects its current status.", "properties": { "id": { "type": "string", - "description": "Stable question identifier used as the key in `answers`" - }, - "title": { - "type": "string", - "description": "Short display title" - }, - "message": { - "type": "string", - "description": "Prompt shown to the user" - }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." }, - "kind": { - "const": "text" + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." }, - "format": { + "name": { "type": "string", - "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" + "description": "Human-readable name." }, - "min": { - "type": "number", - "description": "Minimum string length" + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." }, - "max": { - "type": "number", - "description": "Maximum string length" + "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." }, - "defaultValue": { - "type": "string", - "description": "Default text" - } - }, - "required": [ - "id", - "message", - "kind" - ] - }, - "ChatInputNumberQuestion": { - "type": "object", - "description": "Numeric question within a chat input request.", - "properties": { - "id": { - "type": "string", - "description": "Stable question identifier used as the key in `answers`" + "_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." }, - "title": { - "type": "string", - "description": "Short display title" + "type": { + "const": "mcpServer" }, - "message": { - "type": "string", - "description": "Prompt shown to the user" + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nFlows in both directions. A client publishes this alongside a customization\nto assert its global decision, which is authoritative for the Global scope;\na client always includes its global entry, even when enabled. The host\npublishes the fully resolved set across all scopes, and consumers derive\nthe effective enabled value from that set." }, - "required": { + "isClientBundled": { "type": "boolean", - "description": "Whether the user must answer this question to accept the request" - }, - "kind": { - "oneOf": [ - { - "const": "number" - }, - { - "const": "integer" - } - ] + "description": "Whether the client explicitly bundled this server and owns its Global\nenablement decision." }, - "min": { - "type": "number", - "description": "Minimum value" + "state": { + "$ref": "#/$defs/McpServerState", + "description": "Current lifecycle state of the MCP server." }, - "max": { - "type": "number", - "description": "Maximum value" + "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." }, - "defaultValue": { - "type": "number", - "description": "Default numeric value" + "mcpApp": { + "$ref": "#/$defs/McpServerCustomizationApps", + "description": "MCP App support. This property SHOULD be advertised for MCP servers\nwhich support apps." } }, "required": [ "id", - "message", - "kind" + "uri", + "name", + "type", + "state" ] }, - "ChatInputBooleanQuestion": { + "McpServerCustomizationApps": { "type": "object", - "description": "Boolean question within a chat input request.", + "description": "Information from the agent host needed to render MCP Apps served\nby this MCP server.", "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": "boolean" - }, - "defaultValue": { - "type": "boolean", - "description": "Default boolean value" + "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": [ - "id", - "message", - "kind" + "capabilities" ] }, - "ChatInputSingleSelectQuestion": { + "AhpMcpUiHostCapabilities": { "type": "object", - "description": "Single-select question within a chat input request.", + "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": { - "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" + "serverTools": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `tools/*` methods to the upstream server." }, - "required": { - "type": "boolean", - "description": "Whether the user must answer this question to accept the request" + "serverResources": { + "type": "object", + "properties": { + "listChanged": { + "type": "boolean" + } + }, + "description": "Producer proxies the MCP `resources/*` methods to the upstream server." }, - "kind": { - "const": "single-select" + "logging": { + "type": "object", + "additionalProperties": {}, + "description": "Producer accepts `notifications/message` log entries from the App via `mcpNotification`." }, - "options": { - "type": "array", - "items": { - "$ref": "#/$defs/ChatInputOption" + "sampling": { + "type": "object", + "properties": { + "tools": { + "type": "object", + "additionalProperties": {} + } }, - "description": "Options the user may select from" - }, - "allowFreeformInput": { - "type": "boolean", - "description": "Whether the user may enter text instead of selecting an option" + "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": [ - "id", - "message", - "kind", - "options" + "kind" ] }, - "ChatInputMultiSelectQuestion": { + "McpServerReadyState": { "type": "object", - "description": "Multi-select question within a chat input request.", + "description": "Server is running and serving requests.", "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": "multi-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 in addition to selecting options" - }, - "min": { - "type": "number", - "description": "Minimum selected item count" - }, - "max": { - "type": "number", - "description": "Maximum selected item count" + "const": "ready" } }, "required": [ - "id", - "message", - "kind", - "options" + "kind" ] }, - "ChatInputRequest": { + "McpOAuthClient": { "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 pre-registered OAuth client that clients use instead of dynamic client\nregistration when resolving an MCP authentication challenge.", "properties": { - "id": { + "clientId": { "type": "string", - "description": "Stable request identifier" + "description": "OAuth client identifier registered with the authorization server." }, - "message": { + "clientSecret": { "type": "string", - "description": "Display message for the request as a whole" - }, - "url": { - "$ref": "#/$defs/URI", - "description": "URL the user should review or open, for URL-style elicitations" - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/$defs/ChatInputQuestion" - }, - "description": "Ordered questions to ask the user" - }, - "answers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ChatInputAnswer" - }, - "description": "Current draft or submitted answers, keyed by question ID" + "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" + "clientId" ] }, - "ChatInputTextAnswerValue": { + "McpAuthRequirement": { "type": "object", - "description": "Value captured for one answer.", + "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": { - "kind": { - "const": "text" + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." }, - "value": { - "type": "string" + "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." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." } }, "required": [ - "kind", - "value" + "reason", + "resource" ] }, - "ChatInputNumberAnswerValue": { + "McpServerAuthRequiredState": { "type": "object", + "description": "Server is reachable but cannot serve requests until the client\nauthenticates. Mirrors the discovery flow defined by\n[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)\n(Protected Resource Metadata) and the OAuth 2.1 / RFC 6750 challenge\nsemantics required by the MCP authorization spec.\n\nClients react to this state by calling the existing `authenticate`\ncommand with the {@link ProtectedResourceMetadata.resource | resource}\ncarried here. There is **no** `notify/authRequired` notification for\nMCP servers — the action stream is the single source of truth.\n\nWhen the transition is triggered by a request issued during a turn\n— most commonly\n{@link McpAuthRequiredReason.InsufficientScope | `InsufficientScope`}\nsurfacing mid-tool-call — the host SHOULD also raise\n{@link SessionStatus.InputNeeded} on the session so the block is\nvisible at the summary level. Clients SHOULD watch this status on\nany MCP server backing a running tool call and surface an explicit\naffordance (e.g. a \"grant additional access\" prompt) tied to that\ntool call, rather than relying on the user to notice the\ncustomization’s status badge.", "properties": { - "kind": { - "const": "number" + "reason": { + "$ref": "#/$defs/McpAuthRequiredReason", + "description": "Why authentication is required." }, - "value": { - "type": "number" + "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." + }, + "resource": { + "$ref": "#/$defs/ProtectedResourceMetadata", + "description": "RFC 9728 Protected Resource Metadata. The `resource` field is the\ncanonical MCP server URI per RFC 8707, used as the OAuth `resource`\nindicator. `authorization_servers` is REQUIRED by the MCP\nauthorization spec." + }, + "requiredScopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scopes required for the current challenge, parsed from the\n`WWW-Authenticate: Bearer scope=\"…\"` header (or `scopes_supported`\nfallback). Authoritative for the next authorization request — clients\nMUST NOT assume any subset/superset relationship to\n`resource.scopes_supported`." + }, + "description": { + "type": "string", + "description": "Human-readable hint, typically from the OAuth `error_description`." + }, + "kind": { + "const": "authRequired" } }, "required": [ - "kind", - "value" + "reason", + "resource", + "kind" ] }, - "ChatInputBooleanAnswerValue": { + "McpServerErrorState": { "type": "object", + "description": "Server failed to start, crashed, or otherwise transitioned to a\nnon-recoverable error. Use {@link McpServerStatus.AuthRequired}\nfor authentication failures.", "properties": { "kind": { - "const": "boolean" + "const": "error" }, - "value": { - "type": "boolean" + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Error details." } }, "required": [ "kind", - "value" + "error" ] }, - "ChatInputSelectedAnswerValue": { + "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": "selected" - }, - "value": { - "type": "string" - }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form text entered instead of selecting an option" + "const": "stopped" } }, "required": [ - "kind", - "value" + "kind" ] }, - "ChatInputSelectedManyAnswerValue": { + "ChatState": { "type": "object", + "description": "Full state for a single chat, loaded when a client subscribes to the chat's\nURI.\n\nThe lightweight catalog representation of a chat is {@link ChatSummary},\ncarried in {@link SessionState.chats | `SessionState.chats`}. `ChatState`\n**denormalizes** every {@link ChatSummary} field directly onto itself so\nsubscribers receive one flat object instead of having to merge a nested\n`summary` sub-object. Producers MUST keep the two representations\nconsistent: any change to the inlined fields below SHOULD also be\nannounced on the parent session via the matching\n{@link SessionChatUpdatedAction | `session/chatUpdated`} action.", "properties": { - "kind": { - "const": "selected-many" + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" }, - "value": { - "type": "array", - "items": { - "type": "string" - } + "title": { + "type": "string", + "description": "Chat title" }, - "freeformValues": { + "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\"`)" + }, + "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": "Free-form text entered in addition to selected options" + "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." + }, + "turns": { + "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." + }, + "activeTurn": { + "$ref": "#/$defs/ActiveTurn", + "description": "Currently in-progress turn" + }, + "steeringMessage": { + "$ref": "#/$defs/PendingMessage", + "description": "Message to inject into the current turn at a convenient point" + }, + "queuedMessages": { + "type": "array", + "items": { + "$ref": "#/$defs/PendingMessage" + }, + "description": "Messages to send automatically as new turns after the current turn finishes" + }, + "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." } }, "required": [ - "kind", - "value" + "resource", + "title", + "status", + "modifiedAt", + "turns" ] }, - "ChatInputAnswered": { + "ChatSummary": { "type": "object", + "description": "Lightweight catalog entry for a chat, carried in\n{@link SessionState.chats | `SessionState.chats`}. The full conversation\nlives in {@link ChatState}, which inlines (denormalizes) every field below.", "properties": { - "state": { - "oneOf": [ - { - "const": "draft" - }, - { - "const": "submitted" - } - ], - "description": "Answer state" + "resource": { + "$ref": "#/$defs/URI", + "description": "Chat URI" }, - "value": { - "$ref": "#/$defs/ChatInputAnswerValue", - "description": "Answer value" + "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\"`)" + }, + "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": { + "$ref": "#/$defs/URI" + }, + "description": "The subset of the session's working directories this chat uses.\nSee {@link ChatState.workingDirectories} for the full semantics." } }, "required": [ - "state", - "value" + "resource", + "title", + "status", + "modifiedAt" ] }, - "ChatInputSkipped": { + "SideChatSelection": { "type": "object", + "description": "Immutable selected-text snapshot captured when a side chat is created.\n\nThe host records this exact text when it accepts `createChat`; later changes\nto the source chat do not alter it.", "properties": { - "state": { - "const": "skipped", - "description": "Answer state" + "text": { + "type": "string", + "description": "Exact selected-text snapshot captured at `createChat` acceptance.\n\nMUST be non-empty." }, - "freeformValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Free-form reason or value captured while skipping, if any" + "responsePartId": { + "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`." } }, "required": [ - "state" + "text" ] }, - "Turn": { + "PendingMessage": { "type": "object", - "description": "A completed request/response cycle.", + "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": "Turn identifier" - }, - "startedAt": { - "type": "string", - "description": "ISO 8601 timestamp when this turn started." - }, - "duration": { - "type": "number", - "description": "Turn duration in milliseconds." + "description": "Unique identifier for this pending message" }, "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\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'`" + "description": "The message that will start the next turn" } }, "required": [ "id", - "message", - "responseParts", - "state" + "message" ] }, - "ActiveTurn": { + "ChatInputOption": { "type": "object", - "description": "An in-progress turn — the assistant is actively streaming.", + "description": "A choice in a select-style question.", "properties": { "id": { "type": "string", - "description": "Turn identifier" + "description": "Stable option identifier; for MCP enum values this is the enum string" }, - "startedAt": { + "label": { "type": "string", - "description": "ISO 8601 timestamp when this turn started." - }, - "message": { - "$ref": "#/$defs/Message", - "description": "The message that initiated the turn" + "description": "Display label" }, - "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." + "description": { + "type": "string", + "description": "Optional secondary text" }, - "usage": { - "$ref": "#/$defs/UsageInfo", - "description": "Token usage info" + "recommended": { + "type": "boolean", + "description": "Whether this option is the recommended/default choice" } }, "required": [ "id", - "startedAt", - "message", - "responseParts" + "label" ] }, - "MessageOrigin": { + "ChatInputQuestionBase": { "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.", "properties": { - "kind": { - "$ref": "#/$defs/MessageKind", - "description": "The kind of actor that produced the message." + "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" } }, "required": [ - "kind" + "id", + "message" ] }, - "Message": { + "ChatInputTextQuestion": { "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": "Text question within a chat input request.", "properties": { - "text": { + "id": { "type": "string", - "description": "Message text" + "description": "Stable question identifier used as the key in `answers`" }, - "origin": { - "$ref": "#/$defs/MessageOrigin", - "description": "The origin of the message" + "title": { + "type": "string", + "description": "Short display title" }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/$defs/MessageAttachment" - }, - "description": "File/selection attachments" + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "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." + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "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." + "kind": { + "const": "text" }, - "_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." + "format": { + "type": "string", + "description": "Format hint for text questions, such as `email`, `uri`, `date`, or `date-time`" + }, + "min": { + "type": "number", + "description": "Minimum string length" + }, + "max": { + "type": "number", + "description": "Maximum string length" + }, + "defaultValue": { + "type": "string", + "description": "Default text" } }, "required": [ - "text", - "origin" + "id", + "message", + "kind" ] }, - "MessageAttachmentBase": { + "ChatInputNumberQuestion": { "type": "object", - "description": "Common fields shared by all {@link MessageAttachment} variants.", + "description": "Numeric question within a chat input request.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Stable question identifier used as the key in `answers`" }, - "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." + "title": { + "type": "string", + "description": "Short display title" }, - "displayKind": { + "message": { "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": "Prompt shown to the user" }, - "_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": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" + }, + "kind": { + "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": [ - "label" + "id", + "message", + "kind" ] }, - "SimpleMessageAttachment": { + "ChatInputBooleanQuestion": { "type": "object", - "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", + "description": "Boolean question within a chat input request.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + "description": "Stable question identifier used as the key in `answers`" }, - "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." + "title": { + "type": "string", + "description": "Short display title" }, - "displayKind": { + "message": { "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": "Prompt shown to the user" }, - "_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": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "type": { - "const": "simple", - "description": "Discriminant" + "kind": { + "const": "boolean" }, - "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." + "defaultValue": { + "type": "boolean", + "description": "Default boolean value" } }, "required": [ - "label", - "type" + "id", + "message", + "kind" ] }, - "MessageEmbeddedResourceAttachment": { + "ChatInputSingleSelectQuestion": { "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": "Single-select question within a chat input request.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Stable question identifier used as the key in `answers`" }, - "displayKind": { + "title": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Short display title" }, - "_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." + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "type": { - "const": "embeddedResource", - "description": "Discriminant" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "data": { - "type": "string", - "description": "Base64-encoded binary data" + "kind": { + "const": "single-select" }, - "contentType": { - "type": "string", - "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputOption" + }, + "description": "Options the user may select from" }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text instead of selecting an option" } }, "required": [ - "label", - "type", - "data", - "contentType" + "id", + "message", + "kind", + "options" ] }, - "MessageResourceAttachment": { + "ChatInputMultiSelectQuestion": { "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": "Multi-select question within a chat input request.", "properties": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Stable question identifier used as the key in `answers`" }, - "displayKind": { + "title": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Short display title" }, - "_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." + "message": { + "type": "string", + "description": "Prompt shown to the user" }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Content URI" + "required": { + "type": "boolean", + "description": "Whether the user must answer this question to accept the request" }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" + "kind": { + "const": "multi-select" }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "options": { + "type": "array", + "items": { + "$ref": "#/$defs/ChatInputOption" + }, + "description": "Options the user may select from" }, - "nonce": { - "type": "string", - "description": "Content nonce" + "allowFreeformInput": { + "type": "boolean", + "description": "Whether the user may enter text in addition to selecting options" }, - "type": { - "const": "resource", - "description": "Discriminant" + "min": { + "type": "number", + "description": "Minimum selected item count" }, - "selection": { - "$ref": "#/$defs/TextSelection", - "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + "max": { + "type": "number", + "description": "Maximum selected item count" } }, "required": [ - "label", - "uri", - "type" - ] - }, - "MessageAnnotationsAttachment": { + "id", + "message", + "kind", + "options" + ] + }, + "ChatInputRequest": { "type": "object", - "description": "An attachment that references annotations on a session's annotations\nchannel (see {@link AnnotationsState}).\n\nWhen {@link annotationIds} is omitted the attachment references every\nannotation on the channel; when present it references only the listed\n{@link Annotation.id | annotation ids}.", + "description": "The 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": { - "label": { + "id": { "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + "description": "Stable request identifier" }, - "displayKind": { + "message": { "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." - }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." - }, - "type": { - "const": "annotations", - "description": "Discriminant" + "description": "Display message for the request as a whole" }, - "resource": { + "url": { "$ref": "#/$defs/URI", - "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + "description": "URL the user should review or open, for URL-style elicitations" }, - "annotationIds": { + "questions": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/ChatInputQuestion" }, - "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." - } - }, - "required": [ - "label", - "type", - "resource" - ] - }, - "MessageChatAttachment": { - "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.", - "properties": { - "label": { - "type": "string", - "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." - }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." - }, - "displayKind": { - "type": "string", - "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + "description": "Ordered questions to ask the user" }, - "_meta": { + "answers": { "type": "object", - "additionalProperties": {}, - "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." - }, - "type": { - "const": "chat", - "description": "Discriminant" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "URI of the referenced chat." - }, - "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." + "additionalProperties": { + "$ref": "#/$defs/ChatInputAnswer" + }, + "description": "Current draft or submitted answers, keyed by question ID" } }, "required": [ - "label", - "type", - "resource" + "id" ] }, - "MarkdownResponsePart": { + "ChatInputTextAnswerValue": { "type": "object", + "description": "Value captured for one answer.", "properties": { "kind": { - "const": "markdown", - "description": "Discriminant" - }, - "id": { - "type": "string", - "description": "Part identifier, used by `chat/delta` to target this part for content appends" + "const": "text" }, - "content": { - "type": "string", - "description": "Markdown content" + "value": { + "type": "string" } }, "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" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" - }, - "contentType": { - "type": "string", - "description": "Content MIME type" - }, - "nonce": { - "type": "string", - "description": "Content nonce" - }, - "kind": { - "const": "contentRef", - "description": "Discriminant" - } - }, - "required": [ - "uri", - "kind" + "value" ] }, - "ToolCallResponsePart": { + "ChatInputNumberAnswerValue": { "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" + "const": "number" }, - "toolCall": { - "$ref": "#/$defs/ToolCallState", - "description": "Full tool call lifecycle state" + "value": { + "type": "number" } }, "required": [ "kind", - "toolCall" + "value" ] }, - "ReasoningResponsePart": { + "ChatInputBooleanAnswerValue": { "type": "object", - "description": "Reasoning/thinking content from the model.", "properties": { "kind": { - "const": "reasoning", - "description": "Discriminant" - }, - "id": { - "type": "string", - "description": "Part identifier, used by `chat/reasoning` to target this part for content appends" + "const": "boolean" }, - "content": { - "type": "string", - "description": "Accumulated reasoning text" + "value": { + "type": "boolean" } }, "required": [ "kind", - "id", - "content" + "value" ] }, - "InputRequestResponsePart": { + "ChatInputSelectedAnswerValue": { "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" + "const": "selected" }, - "request": { - "$ref": "#/$defs/ChatInputRequest", - "description": "The request, carrying its `id`, `message`, `url`, `questions`, and current\ndraft or submitted `answers`." + "value": { + "type": "string" }, - "response": { - "$ref": "#/$defs/ChatInputResponseKind", - "description": "How the request was resolved. Absent until a client submits `accept`,\n`decline`, or `cancel` with `chat/inputCompleted`." + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered instead of selecting an option" } }, "required": [ "kind", - "request" + "value" ] }, - "SystemNotificationResponsePart": { + "ChatInputSelectedManyAnswerValue": { "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" + "const": "selected-many" }, - "content": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "The text of the system notification" + "value": { + "type": "array", + "items": { + "type": "string" + } }, - "_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." + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form text entered in addition to selected options" } }, "required": [ "kind", - "content" + "value" ] }, - "ToolCallRiskAssessmentBase": { + "ChatInputAnswered": { "type": "object", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallRiskAssessmentKind" + "state": { + "oneOf": [ + { + "const": "draft" + }, + { + "const": "submitted" + } + ], + "description": "Answer state" + }, + "value": { + "$ref": "#/$defs/ChatInputAnswerValue", + "description": "Answer value" } }, "required": [ - "kind" + "state", + "value" ] }, - "ToolCallRiskAssessmentLoadingState": { + "ChatInputSkipped": { "type": "object", - "description": "The model judge is still evaluating the tool call.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallRiskAssessmentKind" + "state": { + "const": "skipped", + "description": "Answer state" }, - "status": { - "const": "loading" + "freeformValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Free-form reason or value captured while skipping, if any" } }, "required": [ - "kind", - "status" + "state" ] }, - "ToolCallRiskAssessmentCompleteState": { + "Turn": { "type": "object", - "description": "The model judge has completed its evaluation.", + "description": "A completed request/response cycle.", "properties": { - "kind": { - "$ref": "#/$defs/ToolCallRiskAssessmentKind" - }, - "status": { - "const": "complete" + "id": { + "type": "string", + "description": "Turn identifier" }, - "reason": { - "$ref": "#/$defs/StringOrMarkdown" + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." }, - "safety": { + "duration": { "type": "number", - "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." + "description": "Turn duration in milliseconds." + }, + "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\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": [ - "kind", - "status", - "reason", - "safety" + "id", + "message", + "responseParts", + "state" ] }, - "ConfirmationOption": { + "ActiveTurn": { "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": "An in-progress turn — the assistant is actively streaming.", "properties": { "id": { "type": "string", - "description": "Unique identifier for the option, returned in the confirmed action" + "description": "Turn identifier" }, - "label": { + "startedAt": { "type": "string", - "description": "Human-readable label displayed to the user" + "description": "ISO 8601 timestamp when this turn started." }, - "kind": { - "$ref": "#/$defs/ConfirmationOptionKind", - "description": "Whether this option represents an approval or denial" + "message": { + "$ref": "#/$defs/Message", + "description": "The message that initiated the turn" }, - "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." + "responseParts": { + "type": "array", + "items": { + "$ref": "#/$defs/ResponsePart" + }, + "description": "All response content in stream order: text, tool calls, reasoning, and content refs.\n\nTool call parts include `pendingPermissions` when permissions are awaiting user approval." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Token usage info" } }, "required": [ "id", - "label", - "kind" + "startedAt", + "message", + "responseParts" ] }, - "ToolCallClientContributor": { + "MessageOrigin": { "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.", "properties": { "kind": { - "const": "client" + "$ref": "#/$defs/MessageKind", + "description": "The kind of actor that produced the message." + } + }, + "required": [ + "kind" + ] + }, + "Message": { + "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.", + "properties": { + "text": { + "type": "string", + "description": "Message text" }, - "clientId": { + "origin": { + "$ref": "#/$defs/MessageOrigin", + "description": "The origin of the message" + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/$defs/MessageAttachment" + }, + "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." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional provider-specific metadata for this message.\n\nClients MAY look for well-known keys here to provide enhanced UI, and\nagent hosts MAY use it to carry context that does not fit any other\nfield. Mirrors the MCP `_meta` convention." + } + }, + "required": [ + "text", + "origin" + ] + }, + "MessageAttachmentBase": { + "type": "object", + "description": "Common fields shared by all {@link MessageAttachment} variants.", + "properties": { + "label": { "type": "string", - "description": "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": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." } }, "required": [ - "kind", - "clientId" + "label" ] }, - "ToolCallMcpContributor": { + "SimpleMessageAttachment": { "type": "object", + "description": "A simple, opaque attachment whose model representation is described by\nthe producer.", "properties": { - "kind": { - "const": "mcp" + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "type": { + "const": "simple", + "description": "Discriminant" + }, + "modelRepresentation": { + "type": "string", + "description": "Representation of the attachment as it should be shown to the model.\n\nIf the attachment was produced by the client, this property MUST be\ndefined so the agent host can correctly interpret the attachment. This\nproperty MAY be omitted when the attachment originated from a\n`completions` response." + } + }, + "required": [ + "label", + "type" + ] + }, + "MessageEmbeddedResourceAttachment": { + "type": "object", + "description": "An attachment whose data is embedded inline as a base64 string.\n\nUse this for small binary payloads (e.g. a pasted image) that should be\ndelivered with the user message itself rather than fetched separately.", + "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "type": { + "const": "embeddedResource", + "description": "Discriminant" + }, + "data": { + "type": "string", + "description": "Base64-encoded binary data" + }, + "contentType": { + "type": "string", + "description": "Content MIME type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the attached textual resource.\n\nOnly meaningful for textual resources." + } + }, + "required": [ + "label", + "type", + "data", + "contentType" + ] + }, + "MessageResourceAttachment": { + "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.", + "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "type": { + "const": "resource", + "description": "Discriminant" + }, + "selection": { + "$ref": "#/$defs/TextSelection", + "description": "Optional selection within the referenced textual resource.\n\nOnly meaningful for textual resources." + } + }, + "required": [ + "label", + "uri", + "type" + ] + }, + "MessageAnnotationsAttachment": { + "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}.", + "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "type": { + "const": "annotations", + "description": "Discriminant" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "The annotations channel URI (typically `ahp-session://annotations`).\nMatches {@link AnnotationsSummary.resource}." + }, + "annotationIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Specific {@link Annotation.id | annotation ids} to reference. When\nomitted, the attachment references all annotations on the channel." + } + }, + "required": [ + "label", + "type", + "resource" + ] + }, + "MessageChatAttachment": { + "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.", + "properties": { + "label": { + "type": "string", + "description": "A human-readable label for the attachment (e.g. the filename of a file\nattachment). Used for display in UI." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "If defined, the range in {@link Message.text} that references this\nattachment. This is a text range, not a byte range." + }, + "displayKind": { + "type": "string", + "description": "Advisory display hint for clients rendering this attachment. Recognized\nvalues include:\n\n- `'image'`: the attachment is an image\n- `'document'`: the attachment is a textual document\n- `'symbol'`: the attachment is a code symbol (e.g. a function or class)\n- `'directory'`: the attachment is a folder\n- `'selection'`: the attachment is a selection within a document\n\nImplementations MAY provide additional values; clients SHOULD fall back\nto a reasonable default when an unknown value is encountered." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Additional implementation-defined metadata for the attachment.\n\nIf the attachment was produced by the `completions` command, the client\nMUST preserve every property of `_meta` originally returned by the agent\nhost when sending the user message containing the accepted completion." + }, + "type": { + "const": "chat", + "description": "Discriminant" + }, + "resource": { + "$ref": "#/$defs/URI", + "description": "URI of the referenced chat." + }, + "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" + }, + "id": { + "type": "string", + "description": "Part identifier, used by `chat/delta` to target this part for content appends" + }, + "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" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "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" + }, + "toolCall": { + "$ref": "#/$defs/ToolCallState", + "description": "Full tool call lifecycle state" + } + }, + "required": [ + "kind", + "toolCall" + ] + }, + "ReasoningResponsePart": { + "type": "object", + "description": "Reasoning/thinking content from the model.", + "properties": { + "kind": { + "const": "reasoning", + "description": "Discriminant" + }, + "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", + "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" + }, + "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", + "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" + }, + "content": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "The text of the system notification" + }, + "_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", + "content" + ] + }, + "ToolCallRiskAssessmentBase": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + } + }, + "required": [ + "kind" + ] + }, + "ToolCallRiskAssessmentLoadingState": { + "type": "object", + "description": "The model judge is still evaluating the tool call.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "loading" + } + }, + "required": [ + "kind", + "status" + ] + }, + "ToolCallRiskAssessmentCompleteState": { + "type": "object", + "description": "The model judge has completed its evaluation.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "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": [ + "kind", + "status", + "reason", + "safety" + ] + }, + "ConfirmationOption": { + "type": "object", + "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the option, returned in the confirmed action" + }, + "label": { + "type": "string", + "description": "Human-readable label displayed to the user" + }, + "kind": { + "$ref": "#/$defs/ConfirmationOptionKind", + "description": "Whether this option represents an approval or denial" + }, + "group": { + "type": "number", + "description": "Logical group number for visual categorisation.\n\nClients SHOULD display options in the order they are defined and MAY\nuse differing group numbers to insert dividers between logical clusters\nof options." + } + }, + "required": [ + "id", + "label", + "kind" + ] + }, + "ToolCallClientContributor": { + "type": "object", + "properties": { + "kind": { + "const": "client" + }, + "clientId": { + "type": "string", + "description": "If this tool is provided by a client, the `clientId` of the owning client.\nAbsent for server-side tools.\n\nWhen set, the identified client is responsible for executing the tool and\ndispatching `chat/toolCallComplete` with the result." + } + }, + "required": [ + "kind", + "clientId" + ] + }, + "ToolCallMcpContributor": { + "type": "object", + "properties": { + "kind": { + "const": "mcp" + }, + "customizationId": { + "type": "string", + "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + } + }, + "required": [ + "kind", + "customizationId" + ] + }, + "ToolCallBase": { + "type": "object", + "description": "Metadata common to all tool call states.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "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 provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName" + ] + }, + "ToolCallParameterFields": { + "type": "object", + "description": "Properties available once tool call parameters are fully received.", + "properties": { + "invocationMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Message describing what the tool will do" + }, + "toolInput": { + "$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": [ + "invocationMessage" + ] + }, + "ToolCallResult": { + "type": "object", + "description": "Tool execution result details, available after execution completes.", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" + } + }, + "required": [ + "success", + "pastTenseMessage" + ] + }, + "ToolCallStreamingState": { + "type": "object", + "description": "LM is streaming the tool call parameters.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "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 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": [ + "toolCallId", + "toolName", + "displayName", + "status" + ] + }, + "ToolCallPendingConfirmationState": { + "type": "object", + "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "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 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": "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" + }, + "selectedOption": { + "$ref": "#/$defs/ConfirmationOption", + "description": "The confirmation option the user selected, if confirmation options were provided" + } + }, + "required": [ + "confirmed" + ] + }, + "ToolCallRunningState": { + "type": "object", + "description": "Tool is actively executing.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "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 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" + }, + "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": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "confirmed", + "status" + ] + }, + "ToolCallAuthRequiredState": { + "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).", + "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": "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 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" + }, + "status": { + "const": "auth-required" + }, + "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": [ + "toolCallId", + "toolName", + "displayName", + "contributor", + "invocationMessage", + "confirmed", + "status", + "auth" + ] + }, + "ToolCallPendingResultConfirmationState": { + "type": "object", + "description": "Tool finished executing, waiting for client to approve the result.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "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 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." + }, + "success": { + "type": "boolean", + "description": "Whether the tool succeeded" + }, + "pastTenseMessage": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Past-tense description of what the tool did" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/$defs/ToolResultContent" + }, + "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." + }, + "structuredContent": { + "type": "object", + "additionalProperties": {}, + "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message" + ], + "description": "Error details if the tool failed" }, - "customizationId": { - "type": "string", - "description": "Customization ID of the corresponding MCP server in {@link SessionState.customizations}." + "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": [ - "kind", - "customizationId" + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "confirmed", + "status" ] }, - "ToolCallBase": { + "ToolCallCompletedState": { "type": "object", - "description": "Metadata common to all tool call states.", + "description": "Tool completed successfully or with an error.", "properties": { "toolCallId": { "type": "string", @@ -5054,18 +6016,7 @@ "type": "object", "additionalProperties": {}, "description": "Additional provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - } - }, - "required": [ - "toolCallId", - "toolName", - "displayName" - ] - }, - "ToolCallParameterFields": { - "type": "object", - "description": "Properties available once tool call parameters are fully received.", - "properties": { + }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", "description": "Message describing what the tool will do" @@ -5073,16 +6024,7 @@ "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": [ - "invocationMessage" - ] - }, - "ToolCallResult": { - "type": "object", - "description": "Tool execution result details, available after execution completes.", - "properties": { + }, "success": { "type": "boolean", "description": "Whether the tool succeeded" @@ -5117,1285 +6059,1546 @@ "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" + } + }, + "required": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "success", + "pastTenseMessage", + "confirmed", + "status" + ] + }, + "ToolCallCancelledState": { + "type": "object", + "description": "Tool call was cancelled before execution.", + "properties": { + "toolCallId": { + "type": "string", + "description": "Unique tool call identifier" + }, + "toolName": { + "type": "string", + "description": "Internal tool name (for debugging/logging)" + }, + "displayName": { + "type": "string", + "description": "Human-readable tool name" + }, + "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 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": [ + "toolCallId", + "toolName", + "displayName", + "invocationMessage", + "status", + "reason" + ] + }, + "ToolResultTextContent": { + "type": "object", + "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", + "properties": { + "type": { + "const": "text" + }, + "text": { + "type": "string", + "description": "The text content" + } + }, + "required": [ + "type", + "text" + ] + }, + "ToolResultEmbeddedResourceContent": { + "type": "object", + "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", + "properties": { + "type": { + "const": "embeddedResource" + }, + "data": { + "type": "string", + "description": "Base64-encoded data" + }, + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + } + }, + "required": [ + "type", + "data", + "contentType" + ] + }, + "ToolResultResourceContent": { + "type": "object", + "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", + "properties": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Content URI" + }, + "sizeHint": { + "type": "number", + "description": "Approximate size in bytes" + }, + "contentType": { + "type": "string", + "description": "Content MIME type" + }, + "nonce": { + "type": "string", + "description": "Content nonce" + }, + "type": { + "const": "resource" + } + }, + "required": [ + "uri", + "type" + ] + }, + "ToolResultFileEditContent": { + "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." + }, + "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" + }, + "type": { + "const": "fileEdit" } }, "required": [ - "success", - "pastTenseMessage" + "type" ] }, - "ToolCallStreamingState": { + "ToolResultTerminalContent": { "type": "object", - "description": "LM is streaming the tool call parameters.", + "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": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "type": { + "const": "terminal" }, - "displayName": { - "type": "string", - "description": "Human-readable tool name" + "resource": { + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" }, - "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." - }, - "status": { - "const": "streaming" + "description": "Display title for the terminal content" }, - "partialInput": { - "type": "string", - "description": "Partial parameters accumulated from tool-call deltas." + "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." }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Progress message shown while parameters are streaming" + "result": { + "$ref": "#/$defs/TerminalCommandResult", + "description": "Outcome of the command, present once it has exited." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "status" + "type", + "resource", + "title" ] }, - "ToolCallPendingConfirmationState": { + "TerminalCommandResult": { "type": "object", - "description": "Parameters are complete, or a running tool requires re-confirmation\n(e.g. a mid-execution permission check).", + "description": "Outcome of a command run in a terminal-style tool, filled in on\n{@link ToolResultTerminalContent.result} once the command exits.", "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" + "exitCode": { + "type": "number", + "description": "Exit code from the completed command, if reported by the runtime" }, - "intention": { + "preview": { "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": "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." }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "truncated": { + "type": "boolean", + "description": "Whether `preview` is known to be incomplete or truncated" + } + } + }, + "ToolResultSubagentContent": { + "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" }, - "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." + "resource": { + "$ref": "#/$defs/URI", + "description": "Worker chat URI (subscribable for full chat state)" }, - "status": { - "const": "pending-confirmation" + "title": { + "type": "string", + "description": "Display title for the subagent" }, - "confirmationTitle": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" + "agentName": { + "type": "string", + "description": "Internal agent name" }, - "riskAssessment": { - "$ref": "#/$defs/ToolCallRiskAssessment", - "description": "Risk assessment that informed the confirmation requirement." + "description": { + "type": "string", + "description": "Human-readable description of the subagent's task" + } + }, + "required": [ + "type", + "resource", + "title" + ] + }, + "TerminalInfo": { + "type": "object", + "description": "Lightweight terminal metadata exposed on the root state.", + "properties": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Terminal URI (subscribable for full terminal state)" }, - "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" + "title": { + "type": "string", + "description": "Human-readable terminal title" }, - "editable": { - "type": "boolean", - "description": "Whether the agent host allows the client to edit the tool's input parameters before confirming" + "claim": { + "$ref": "#/$defs/TerminalClaim", + "description": "Who currently holds this terminal" }, - "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." + "exitCode": { + "type": "number", + "description": "Process exit code, if the terminal process has exited" } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status" + "resource", + "title", + "claim" ] }, - "ToolCallPostConfirmationFields": { + "TerminalClientClaim": { "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": "A terminal claimed by a connected client.", "properties": { - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "kind": { + "const": "client", + "description": "Discriminant" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "clientId": { + "type": "string", + "description": "The `clientId` of the claiming client" } }, "required": [ - "confirmed" + "kind", + "clientId" ] }, - "ToolCallRunningState": { + "TerminalSessionClaim": { "type": "object", - "description": "Tool is actively executing.", + "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" + "kind": { + "const": "session", + "description": "Discriminant" }, - "toolName": { - "type": "string", - "description": "Internal tool name (for debugging/logging)" + "session": { + "$ref": "#/$defs/URI", + "description": "Session URI that claimed the terminal" }, - "displayName": { + "turnId": { "type": "string", - "description": "Human-readable tool name" + "description": "Optional turn identifier within the session" }, - "intention": { + "toolCallId": { "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." - }, - "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": "Optional tool call identifier within the turn" + } + }, + "required": [ + "kind", + "session" + ] + }, + "TerminalState": { + "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" }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "cwd": { + "$ref": "#/$defs/URI", + "description": "Current working directory of the terminal process" }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "cols": { + "type": "number", + "description": "Terminal width in columns" }, - "status": { - "const": "running" + "rows": { + "type": "number", + "description": "Terminal height in rows" }, "content": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" + "$ref": "#/$defs/TerminalContentPart" }, - "description": "Partial content produced while the tool is still executing.\n\nFor example, a terminal content block lets clients subscribe to live\noutput before the tool completes." + "description": "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." + }, + "exitCode": { + "type": "number", + "description": "Process exit code, set when the terminal process exits" + }, + "claim": { + "$ref": "#/$defs/TerminalClaim", + "description": "Who currently holds this terminal" + }, + "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." + }, + "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", - "confirmed", - "status" + "title", + "content", + "claim" ] }, - "ToolCallAuthRequiredState": { + "TerminalUnclassifiedPart": { "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": "Unstructured terminal output — content before, between, or after commands,\nor from terminals that do not support command detection.", "properties": { - "toolCallId": { + "type": { "type": "string", - "description": "Unique tool call identifier" + "enum": [ + "unclassified" + ] }, - "toolName": { + "value": { "type": "string", - "description": "Internal tool name (for debugging/logging)" - }, - "displayName": { + "description": "Accumulated VT output. Appended to by `terminal/data` when no command is executing." + } + }, + "required": [ + "type", + "value" + ] + }, + "TerminalCommandPart": { + "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", - "description": "Human-readable tool name" + "enum": [ + "command" + ] }, - "intention": { + "commandId": { "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 provider-specific metadata for this tool call.\n\nThis MAY include a `ui` field corresponding to the MCP Apps (SEP-1865)\n`McpUiToolMeta` found in MCP tool calls, which may be used in combination\nwith the {@link contributor} to serve MCP Apps." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Stable id matching the `commandId` on the corresponding\n`terminal/commandExecuted` and `terminal/commandFinished` actions." }, - "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." + "commandLine": { + "type": "string", + "description": "The command line submitted to the shell." }, - "confirmed": { - "$ref": "#/$defs/ToolCallConfirmationReason", - "description": "How the tool was confirmed for execution" + "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." }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "timestamp": { + "type": "number", + "description": "Unix timestamp (ms) when execution started, as reported by the server." }, - "status": { - "const": "auth-required" + "isComplete": { + "type": "boolean", + "description": "Whether the command has finished." }, - "auth": { - "$ref": "#/$defs/McpAuthRequirement", - "description": "The authentication challenge blocking this invocation." + "exitCode": { + "type": "number", + "description": "Shell exit code. Set at completion. `undefined` if unknown." }, - "content": { - "type": "array", - "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Partial content produced before the call paused for authentication." + "durationMs": { + "type": "number", + "description": "Wall-clock duration in milliseconds. Set at completion." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "contributor", - "invocationMessage", - "confirmed", - "status", - "auth" + "type", + "commandId", + "commandLine", + "output", + "timestamp", + "isComplete" ] }, - "ToolCallPendingResultConfirmationState": { + "Changeset": { "type": "object", - "description": "Tool finished executing, waiting for client to approve the result.", + "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/ToolCallContributor", - "description": "Reference to the contributor of the tool being called." + "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." - }, - "success": { - "type": "boolean", - "description": "Whether the tool succeeded" + "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": { + "$ref": "#/$defs/ChangesetStatus", + "description": "Computation lifecycle." }, - "pastTenseMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Past-tense description of what the tool did" + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Present iff `status === ChangesetStatus.Error`." }, - "content": { + "files": { "type": "array", "items": { - "$ref": "#/$defs/ToolResultContent" - }, - "description": "Unstructured result content blocks.\n\nThis mirrors the `content` field of MCP `CallToolResult`." - }, - "structuredContent": { - "type": "object", - "additionalProperties": {}, - "description": "Optional structured result object.\n\nThis mirrors the `structuredContent` field of MCP `CallToolResult`." - }, - "error": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "code": { - "type": "string" - } + "$ref": "#/$defs/ChangesetFile" }, - "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" + "description": "Files in this changeset, keyed by {@link ChangesetFile.id}." }, - "status": { - "const": "pending-result-confirmation" + "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", - "invocationMessage", - "success", - "pastTenseMessage", - "confirmed", - "status" + "status", + "files" ] }, - "ToolCallCompletedState": { + "ChangesetFile": { "type": "object", - "description": "Tool completed successfully or with an error.", + "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": { - "type": "string", - "description": "Human-readable tool name" - }, - "intention": { + "id": { "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." - }, - "invocationMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "description": "Stable identifier within the changeset. Typically `after.uri`\n(or `before.uri` for deletions)." }, - "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." + "edit": { + "$ref": "#/$defs/FileEdit", + "description": "Reuses the existing {@link FileEdit} shape. Clients derive line\nadditions, deletions, and rename/create/delete semantics from this." }, - "success": { + "reviewed": { "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`." + "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`." }, - "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": "Server-defined opaque metadata, surfaced to operations and tooling\nbut not interpreted by the protocol." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "success", - "pastTenseMessage", - "confirmed", - "status" + "id", + "edit" ] }, - "ToolCallCancelledState": { + "ChangesetOperation": { "type": "object", - "description": "Tool call was cancelled before execution.", + "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": { - "toolCallId": { - "type": "string", - "description": "Unique tool call identifier" - }, - "toolName": { + "id": { "type": "string", - "description": "Internal tool name (for debugging/logging)" + "description": "Stable identifier, unique within this changeset." }, - "displayName": { + "label": { "type": "string", - "description": "Human-readable tool name" + "description": "Human-readable button/menu label." }, - "intention": { + "description": { "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": "Optional longer description shown on hover or in tooltips." }, - "_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." + "scopes": { + "type": "array", + "items": { + "$ref": "#/$defs/ChangesetOperationScope" + }, + "description": "Where this operation can be invoked." }, - "invocationMessage": { + "confirmation": { "$ref": "#/$defs/StringOrMarkdown", - "description": "Message describing what the tool will do" + "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)." }, - "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." + "icon": { + "type": "string", + "description": "Optional generic icon hint, e.g. `\"check\"`, `\"trash\"`." }, - "status": { - "const": "cancelled" + "group": { + "type": "string", + "description": "Optional group identifier, used to group related operations together." }, - "reason": { - "$ref": "#/$defs/ToolCallCancellationReason", - "description": "Why the tool was cancelled" + "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`." }, - "reasonMessage": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional message explaining the cancellation" + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Cause of failure. Present iff\n`status === ChangesetOperationStatus.Error`; otherwise omitted." + } + }, + "required": [ + "id", + "label", + "scopes", + "status" + ] + }, + "AnnotationsSummary": { + "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." }, - "userSuggestion": { - "$ref": "#/$defs/Message", - "description": "What the user suggested doing instead" + "annotationCount": { + "type": "number", + "description": "Total number of {@link Annotation} entries in the channel." }, - "selectedOption": { - "$ref": "#/$defs/ConfirmationOption", - "description": "The confirmation option the user selected, if confirmation options were provided" + "entryCount": { + "type": "number", + "description": "Total number of {@link AnnotationEntry} entries across every annotation." } }, "required": [ - "toolCallId", - "toolName", - "displayName", - "invocationMessage", - "status", - "reason" + "resource", + "annotationCount", + "entryCount" ] }, - "ToolResultTextContent": { + "AnnotationsState": { "type": "object", - "description": "Text content in a tool result.\n\nMirrors MCP `TextContent`.", + "description": "Full state for a session's annotations channel, returned when a client\nsubscribes to an `ahp-session://annotations` URI.", "properties": { - "type": { - "const": "text" + "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": "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": { + "$ref": "#/$defs/URI", + "description": "The file the annotation is anchored to." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Range within {@link resource} the annotation is anchored to. When\nomitted the annotation is anchored to the entire file." + }, + "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)." + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/$defs/AnnotationEntry" + }, + "description": "Entries in this annotation, in dispatch order (oldest first). MUST\ncontain at least one entry." }, - "text": { - "type": "string", - "description": "The text content" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." } }, "required": [ - "type", - "text" + "id", + "turnId", + "resource", + "resolved", + "entries" ] }, - "ToolResultEmbeddedResourceContent": { + "AnnotationEntry": { "type": "object", - "description": "Base64-encoded binary content embedded in a tool result.\n\nMirrors MCP `EmbeddedResource` for inline binary data.", + "description": "A single entry within an {@link Annotation}.", "properties": { - "type": { - "const": "embeddedResource" - }, - "data": { + "id": { "type": "string", - "description": "Base64-encoded data" + "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." }, - "contentType": { - "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"application/pdf\"`)" + "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": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." } }, "required": [ - "type", - "data", - "contentType" + "id", + "text" ] }, - "ToolResultResourceContent": { + "TelemetryCapabilities": { "type": "object", - "description": "A reference to a resource stored outside the tool result.\n\nWraps {@link ContentRef} for lazy-loading large results.", + "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": { - "uri": { + "logs": { "$ref": "#/$defs/URI", - "description": "Content URI" - }, - "sizeHint": { - "type": "number", - "description": "Approximate size in bytes" - }, - "contentType": { - "type": "string", - "description": "Content MIME type" + "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)." }, - "nonce": { - "type": "string", - "description": "Content nonce" + "traces": { + "$ref": "#/$defs/URI", + "description": "Channel URI for OTLP spans (`otlp/exportTraces` notifications). No\ntemplate variables are defined by this protocol version." }, - "type": { - "const": "resource" + "metrics": { + "$ref": "#/$defs/URI", + "description": "Channel URI for OTLP metric data points (`otlp/exportMetrics`\nnotifications). No template variables are defined by this protocol\nversion." } - }, - "required": [ - "uri", - "type" - ] + } }, - "ToolResultFileEditContent": { + "ResourceWatchState": { "type": "object", - "description": "Describes a file modification performed by a tool.", + "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": { - "before": { + "root": { + "$ref": "#/$defs/URI", + "description": "The URI being watched. For recursive watches this is the root of the\nsubtree; for non-recursive watches this is the single file or\ndirectory." + }, + "recursive": { + "type": "boolean", + "description": "`true` if the watcher reports changes for descendants of `root`;\n`false` if it only reports changes to `root` itself (and, when\n`root` is a directory, its direct children)." + }, + "excludes": { "type": "object", "properties": { - "uri": { - "$ref": "#/$defs/URI" - }, - "content": { - "$ref": "#/$defs/ContentRef" + "items": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "uri", - "content" + "items" ], - "description": "The file state before the edit. Absent for file creations or for in-place file edits." + "description": "Optional glob patterns or paths relative to `root` to exclude from\nchange reporting." }, - "after": { + "includes": { "type": "object", "properties": { - "uri": { - "$ref": "#/$defs/URI" - }, - "content": { - "$ref": "#/$defs/ContentRef" + "items": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "uri", - "content" + "items" ], - "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" - }, - "type": { - "const": "fileEdit" + "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": [ - "type" + "root", + "recursive" ] }, - "ToolResultTerminalContent": { + "ResourceChange": { "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.", + "description": "A single change observed by a resource watcher.", "properties": { - "type": { - "const": "terminal" - }, - "resource": { + "uri": { "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" - }, - "title": { - "type": "string", - "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." + "description": "The URI of the resource that changed." }, - "result": { - "$ref": "#/$defs/TerminalCommandResult", - "description": "Outcome of the command, present once it has exited." + "type": { + "$ref": "#/$defs/ResourceChangeType", + "description": "The kind of change observed." } }, "required": [ - "type", - "resource", - "title" + "uri", + "type" ] }, - "TerminalCommandResult": { + "AutomationSchedule": { "type": "object", - "description": "Outcome of a command run in a terminal-style tool, filled in on\n{@link ToolResultTerminalContent.result} once the command exits.", + "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": { - "exitCode": { - "type": "number", - "description": "Exit code from the completed command, if reported by the runtime" - }, - "preview": { + "expression": { "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." + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, - "truncated": { - "type": "boolean", - "description": "Whether `preview` is known to be incomplete or truncated" + "timeZone": { + "type": "string", + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } - } + }, + "required": [ + "expression", + "timeZone" + ] }, - "ToolResultSubagentContent": { + "AutomationScheduleTrigger": { "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.", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", "properties": { - "type": { - "const": "subagent" - }, - "resource": { - "$ref": "#/$defs/URI", - "description": "Worker chat URI (subscribable for full chat state)" - }, - "title": { + "id": { "type": "string", - "description": "Display title for the subagent" + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - "agentName": { - "type": "string", - "description": "Internal agent name" + "kind": { + "const": "schedule" }, - "description": { - "type": "string", - "description": "Human-readable description of the subagent's task" + "schedule": { + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." + }, + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } }, "required": [ - "type", - "resource", - "title" + "id", + "kind", + "schedule" ] }, - "TerminalInfo": { + "AutomationEventTrigger": { "type": "object", - "description": "Lightweight terminal metadata exposed on the root state.", + "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": { - "resource": { - "$ref": "#/$defs/URI", - "description": "Terminal URI (subscribable for full terminal state)" + "id": { + "type": "string", + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - "title": { + "kind": { + "const": "event" + }, + "type": { "type": "string", - "description": "Human-readable terminal title" + "description": "Matches {@link AutomationTriggerDefinition.type}." }, - "claim": { - "$ref": "#/$defs/TerminalClaim", - "description": "Who currently holds this terminal" + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, - "exitCode": { - "type": "number", - "description": "Process exit code, if the terminal process has exited" + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } }, "required": [ - "resource", - "title", - "claim" + "id", + "kind", + "type", + "events" ] }, - "TerminalClientClaim": { + "AutomationTriggerEventDefinition": { "type": "object", - "description": "A terminal claimed by a connected client.", + "description": "One selectable event exposed by a host-defined trigger type.", "properties": { - "kind": { - "const": "client", - "description": "Discriminant" + "id": { + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, - "clientId": { + "title": { "type": "string", - "description": "The `clientId` of the claiming client" + "description": "Human-readable label suitable for selection UI." + }, + "description": { + "type": "string", + "description": "Optional longer explanation of when this event fires." } }, - "required": [ - "kind", - "clientId" + "required": [ + "id", + "title" ] }, - "TerminalSessionClaim": { + "AutomationTriggerDefinition": { "type": "object", - "description": "A terminal claimed by a session, optionally scoped to a specific turn or tool call.", + "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": { - "kind": { - "const": "session", - "description": "Discriminant" - }, - "session": { - "$ref": "#/$defs/URI", - "description": "Session URI that claimed the terminal" + "type": { + "type": "string", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, - "turnId": { + "title": { "type": "string", - "description": "Optional turn identifier within the session" + "description": "Human-readable trigger type name." }, - "toolCallId": { + "description": { "type": "string", - "description": "Optional tool call identifier within the turn" + "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", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } }, "required": [ - "kind", - "session" + "type", + "title", + "events" ] }, - "TerminalState": { + "AutomationSessionTemplate": { "type": "object", - "description": "Full state for a single terminal, loaded when a client subscribes to the terminal's URI.", + "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": { - "title": { + "provider": { "type": "string", - "description": "Human-readable terminal title" - }, - "cwd": { - "$ref": "#/$defs/URI", - "description": "Current working directory of the terminal process" + "description": "Provider id. Omit to use the host's default provider." }, - "cols": { - "type": "number", - "description": "Terminal width in columns" + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." }, - "rows": { - "type": "number", - "description": "Terminal height in rows" + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." }, - "content": { + "workingDirectories": { "type": "array", "items": { - "$ref": "#/$defs/TerminalContentPart" + "$ref": "#/$defs/URI" }, - "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": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." }, - "exitCode": { - "type": "number", - "description": "Process exit code, set when the terminal process exits" + "config": { + "type": "object", + "additionalProperties": {}, + "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", + "description": "Human-readable automation name." }, - "claim": { - "$ref": "#/$defs/TerminalClaim", - "description": "Who currently holds this terminal" + "message": { + "$ref": "#/$defs/Message", + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, - "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." + "session": { + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, - "isPty": { + "enabled": { "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." + "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": "Automatic triggers. An empty list means manual-only." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." } }, "required": [ "title", - "content", - "claim" + "message", + "session", + "enabled", + "triggers" ] }, - "TerminalUnclassifiedPart": { + "AutomationRuntimeState": { "type": "object", - "description": "Unstructured terminal output — content before, between, or after commands,\nor from terminals that do not support command detection.", + "description": "Host-resolved execution context that is useful to clients but is not part of\nthe editable definition.", "properties": { - "type": { - "type": "string", - "enum": [ - "unclassified" - ] + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Effective working directories after host-side preparation, such as\nmaterializing a managed workspace." }, - "value": { - "type": "string", - "description": "Accumulated VT output. Appended to by `terminal/data` when no command is executing." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined runtime metadata." } - }, - "required": [ - "type", - "value" - ] + } }, - "TerminalCommandPart": { + "AutomationSummary": { "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.", + "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": { - "type": { - "type": "string", - "enum": [ - "command" - ] + "resource": { + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation:` URI." }, - "commandId": { + "title": { "type": "string", - "description": "Stable id matching the `commandId` on the corresponding\n`terminal/commandExecuted` and `terminal/commandFinished` actions." + "description": "Current {@link AutomationDefinition.title}." }, - "commandLine": { - "type": "string", - "description": "The command line submitted to the shell." + "enabled": { + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." }, - "output": { + "triggerCount": { + "type": "number", + "description": "Number of automatic triggers in the current definition." + }, + "nextRunAt": { "type": "string", - "description": "Accumulated VT output. Appended to by `terminal/data` while `isComplete`\nis false. Shell integration escape sequences are stripped by the server." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, - "timestamp": { + "lastRun": { + "$ref": "#/$defs/AutomationRunSummary", + "description": "Most recent retained run, when any run exists." + }, + "revision": { "type": "number", - "description": "Unix timestamp (ms) when execution started, as reported by the server." + "description": "Monotonic definition revision used for optimistic concurrency." }, - "isComplete": { - "type": "boolean", - "description": "Whether the command has finished." + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationOperation" + }, + "description": "Operations currently permitted for this automation." }, - "exitCode": { - "type": "number", - "description": "Shell exit code. Set at completion. `undefined` if unknown." + "createdAt": { + "type": "string", + "description": "Creation timestamp in ISO 8601 format." }, - "durationMs": { - "type": "number", - "description": "Wall-clock duration in milliseconds. Set at completion." + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." } }, "required": [ - "type", - "commandId", - "commandLine", - "output", - "timestamp", - "isComplete" + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" ] }, - "Changeset": { + "AutomationState": { "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}.", + "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": { - "label": { + "resource": { + "$ref": "#/$defs/URI", + "description": "URI of this automation channel." + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." + }, + "revision": { + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." + }, + "nextRunAt": { "type": "string", - "description": "Human-readable label, e.g. `\"Uncommitted Changes\"`." + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, - "uriTemplate": { + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." + }, + "runsNextCursor": { "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." + "description": "Opaque cursor for the next older run-history page." }, - "description": { + "runtime": { + "$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", - "description": "Optional longer description." + "description": "Creation timestamp in ISO 8601 format." }, - "changeKind": { + "modifiedAt": { "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." + "description": "Last definition modification timestamp in ISO 8601 format." }, - "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`." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." } }, "required": [ - "label", - "uriTemplate", - "changeKind" + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" ] }, - "ChangesetCapabilities": { + "AutomationRunBlocker": { "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.", + "description": "Summary of why a run cannot currently make progress.", "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." + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." } - } + }, + "required": [ + "kind" + ] }, - "ChangesetState": { + "AutomationManualRunCause": { "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`.", + "description": "Cause recorded for a client-requested manual run.", "properties": { - "status": { - "$ref": "#/$defs/ChangesetStatus", - "description": "Computation lifecycle." - }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Present iff `status === ChangesetStatus.Error`." - }, - "files": { - "type": "array", - "items": { - "$ref": "#/$defs/ChangesetFile" - }, - "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." + "kind": { + "const": "manual" } }, "required": [ - "status", - "files" + "kind" ] }, - "ChangesetFile": { + "AutomationTriggeredRunCause": { "type": "object", - "description": "One file entry within a {@link ChangesetState}.", + "description": "Cause recorded for a run created by one of the automation's triggers.", "properties": { - "id": { + "kind": { + "const": "trigger" + }, + "triggerId": { "type": "string", - "description": "Stable identifier within the changeset. Typically `after.uri`\n(or `before.uri` for deletions)." + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." }, - "edit": { - "$ref": "#/$defs/FileEdit", - "description": "Reuses the existing {@link FileEdit} shape. Clients derive line\nadditions, deletions, and rename/create/delete semantics from this." + "scheduledFor": { + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." }, - "reviewed": { + "catchUp": { "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`." + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." }, - "_meta": { + "event": { "type": "object", "additionalProperties": {}, - "description": "Server-defined opaque metadata, surfaced to operations and tooling\nbut not interpreted by the protocol." + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." } }, "required": [ - "id", - "edit" + "kind", + "triggerId" ] }, - "ChangesetOperation": { + "AutomationPendingRunLifecycle": { "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.", + "description": "A durable run exists but has not begun external execution.", "properties": { - "id": { - "type": "string", - "description": "Stable identifier, unique within this changeset." - }, - "label": { - "type": "string", - "description": "Human-readable button/menu label." + "status": { + "const": "pending" }, - "description": { + "createdAt": { "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)." + "description": "Run creation timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "description": "The run is actively executing linked sessions.", + "properties": { + "status": { + "const": "running" }, - "icon": { + "createdAt": { "type": "string", - "description": "Optional generic icon hint, e.g. `\"check\"`, `\"trash\"`." + "description": "Run creation timestamp in ISO 8601 format." }, - "group": { + "startedAt": { "type": "string", - "description": "Optional group identifier, used to group related operations together." - }, - "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`." - }, - "error": { - "$ref": "#/$defs/ErrorInfo", - "description": "Cause of failure. Present iff\n`status === ChangesetOperationStatus.Error`; otherwise omitted." + "description": "First execution start timestamp in ISO 8601 format." } }, "required": [ - "id", - "label", - "scopes", - "status" + "status", + "createdAt", + "startedAt" ] }, - "AnnotationsSummary": { + "AutomationBlockedRunLifecycle": { "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.", + "description": "The run started but is temporarily unable to progress.", "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": "blocked" }, - "annotationCount": { - "type": "number", - "description": "Total number of {@link Annotation} entries in the channel." + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, - "entryCount": { - "type": "number", - "description": "Total number of {@link AnnotationEntry} entries across every annotation." + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." } }, "required": [ - "resource", - "annotationCount", - "entryCount" + "status", + "createdAt", + "startedAt", + "blocker" ] }, - "AnnotationsState": { + "AutomationCompletedRunLifecycle": { "type": "object", - "description": "Full state for a session's annotations channel, returned when a client\nsubscribes to an `ahp-session://annotations` URI.", + "description": "Terminal lifecycle for a successfully completed run.", "properties": { - "annotations": { - "type": "array", - "items": { - "$ref": "#/$defs/Annotation" - }, - "description": "Annotations in this channel, keyed by {@link Annotation.id}." + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "completedAt": { + "type": "string", + "description": "Completion timestamp in ISO 8601 format." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." } }, "required": [ - "annotations" + "status", + "createdAt", + "startedAt", + "completedAt" ] }, - "Annotation": { + "AutomationFailedRunLifecycle": { "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.", + "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": { - "id": { + "status": { + "const": "failed" + }, + "createdAt": { "type": "string", - "description": "Stable identifier within the annotations channel. Assigned by the client\nthat dispatches the creating {@link AnnotationsSetAction}." + "description": "Run creation timestamp in ISO 8601 format." }, - "turnId": { + "startedAt": { "type": "string", - "description": "Turn that produced the file versions this annotation is anchored to.\nMatches a {@link Turn.id} on the owning session." + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, - "resource": { - "$ref": "#/$defs/URI", - "description": "The file the annotation is anchored to." + "completedAt": { + "type": "string", + "description": "Failure timestamp in ISO 8601 format." }, - "range": { - "$ref": "#/$defs/TextRange", - "description": "Range within {@link resource} the annotation is anchored to. When\nomitted the annotation is anchored to the entire file." + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "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" }, - "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)." + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." }, - "entries": { - "type": "array", - "items": { - "$ref": "#/$defs/AnnotationEntry" - }, - "description": "Entries in this annotation, in dispatch order (oldest first). MUST\ncontain at least one entry." + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." + "completedAt": { + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." } }, "required": [ - "id", - "turnId", - "resource", - "resolved", - "entries" + "status", + "createdAt", + "completedAt" ] }, - "AnnotationEntry": { + "AutomationRunArtifact": { "type": "object", - "description": "A single entry within an {@link Annotation}.", + "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", + "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", - "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." + "description": "Stable artifact id within this run, used by artifact actions." }, - "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", + "description": "Human-readable label suitable for run-history UI." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Producer-defined opaque metadata, surfaced to tooling but not\ninterpreted by the protocol." + "description": "Opaque host-defined artifact metadata." } }, "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`).", + "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": { - "logs": { + "resource": { "$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)." + "description": "Subscribable `ahp-automation-run:` URI." }, - "traces": { + "automation": { "$ref": "#/$defs/URI", - "description": "Channel URI for OTLP spans (`otlp/exportTraces` notifications). No\ntemplate variables are defined by this protocol version." + "description": "Owning `ahp-automation:` 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": { + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." + }, + "primarySession": { "$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." + "description": "Session the host recommends opening first, when one has been selected." }, - "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)." + "sessionCount": { + "type": "number", + "description": "Number of linked sessions, including attempts and workers." }, - "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." + "artifactCount": { + "type": "number", + "description": "Number of run-scoped artifacts, when cheaply available." }, - "includes": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "string" - } - } + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" }, - "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`." + "description": "Operations currently permitted for this run." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined summary metadata." } }, "required": [ - "root", - "recursive" + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" ] }, - "ResourceChange": { + "AutomationRunState": { "type": "object", - "description": "A single change observed by a resource watcher.", + "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": { - "uri": { + "resource": { "$ref": "#/$defs/URI", - "description": "The URI of the resource that changed." + "description": "URI of this automation-run channel." }, - "type": { - "$ref": "#/$defs/ResourceChangeType", - "description": "The kind of change observed." + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$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", + "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": {}, + "description": "Opaque host-defined run metadata." } }, "required": [ - "uri", - "type" + "resource", + "automation", + "cause", + "lifecycle", + "sessions", + "artifacts", + "operations" ] }, "ActionOrigin": { @@ -8483,10 +9686,228 @@ "changes" ] }, + "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", + "description": "Complete replacement definition." + }, + "revision": { + "type": "number", + "description": "New monotonic revision." + }, + "modifiedAt": { + "type": "string", + "description": "Definition modification timestamp in ISO 8601 format." + }, + "nextRunAt": { + "type": "string", + "description": "Earliest known future scheduled occurrence, or omitted to clear it." + } + }, + "required": [ + "type", + "definition", + "revision", + "modifiedAt" + ] + }, + "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", + "description": "New or replacement run summary." + } + }, + "required": [ + "type", + "run" + ] + }, + "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", + "description": "{@link AutomationRunSummary.resource} to remove." + } + }, + "required": [ + "type", + "run" + ] + }, + "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" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Older run summaries in newest-first order within this page." + }, + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next older page, or omitted at the end." + } + }, + "required": [ + "type", + "runs" + ] + }, + "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", + "description": "Complete replacement lifecycle." + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + }, + "description": "Complete replacement operation list." + } + }, + "required": [ + "type", + "lifecycle", + "operations" + ] + }, + "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", + "description": "Session URI to append when it is not already linked." + } + }, + "required": [ + "type", + "session" + ] + }, + "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", + "description": "Linked session URI to remove." + } + }, + "required": [ + "type", + "session" + ] + }, + "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", + "description": "New primary linked session, or omitted to clear the selection." + } + }, + "required": [ + "type" + ] + }, + "AutomationRunArtifactSetAction": { + "type": "object", + "description": "Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}.", + "properties": { + "type": { + "const": "automationRun/artifactSet" + }, + "artifact": { + "$ref": "#/$defs/AutomationRunArtifact", + "description": "New or replacement artifact." + } + }, + "required": [ + "type", + "artifact" + ] + }, + "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", + "description": "{@link AutomationRunArtifact.id} to remove." + } + }, + "required": [ + "type", + "artifactId" + ] + }, + "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" + } + }, + "required": [ + "type" + ] + }, "URI": { "type": "string", "description": "A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)." }, + "AutomationExecutionLifetime": { + "enum": [ + "hostLifetime", + "managed" + ], + "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": [ { @@ -8743,6 +10164,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." @@ -8894,6 +10348,17 @@ ], "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" + } + ], + "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": [ { @@ -8946,6 +10411,10 @@ "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", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." + }, "SessionLifecycle": { "enum": [ "creating", @@ -9475,6 +10944,74 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "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": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "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": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ], + "description": "Immutable provenance describing why a run was created." + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ], + "description": "Discriminated lifecycle of an automation run." + }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string", + "description": "Operations the host currently permits for a run." + }, "PendingMessageKind": { "enum": [ "steering", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index faf6b0288..73082060b 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,28 @@ "values" ] }, + "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", + "description": "Owning `ahp-automation:` URI." + }, + "run": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` 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 +782,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 +828,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 +1136,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" @@ -4994,384 +5034,1501 @@ "type" ] }, - "BaseParams": { + "AutomationSchedule": { "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.", + "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": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." + "expression": { + "type": "string", + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + "timeZone": { + "type": "string", + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } }, "required": [ - "channel" + "expression", + "timeZone" ] }, - "PaginatedParams": { + "AutomationScheduleTrigger": { "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.", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", "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": { + "id": { "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." + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." + }, + "kind": { + "const": "schedule" + }, + "schedule": { + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." + }, + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } - } + }, + "required": [ + "id", + "kind", + "schedule" + ] }, - "PaginatedResult": { + "AutomationEventTrigger": { "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.", + "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": { - "nextCursor": { + "id": { "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": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." + }, + "kind": { + "const": "event" + }, + "type": { + "type": "string", + "description": "Matches {@link AutomationTriggerDefinition.type}." + }, + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } - } + }, + "required": [ + "id", + "kind", + "type", + "events" + ] }, - "Implementation": { + "AutomationTriggerEventDefinition": { "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`}.", + "description": "One selectable event exposed by a host-defined trigger type.", "properties": { - "name": { + "id": { "type": "string", - "description": "Implementation name, e.g. a product or package identifier." + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, - "version": { + "title": { "type": "string", - "description": "Implementation version. A [SemVer](https://semver.org) string is\nrecommended but not required." + "description": "Human-readable label suitable for selection UI." }, - "title": { + "description": { "type": "string", - "description": "Optional human-readable display name." + "description": "Optional longer explanation of when this event fires." } }, "required": [ - "name" + "id", + "title" ] }, - "InitializeParams": { + "AutomationTriggerDefinition": { "type": "object", - "description": "Establishes a new connection and negotiates the protocol version.\nThis MUST be the first message sent by the client.", + "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": { - "channel": { + "type": { "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`)." + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, - "clientId": { + "title": { "type": "string", - "description": "Unique client identifier" + "description": "Human-readable trigger type name." }, - "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." + "description": { + "type": "string", + "description": "Optional longer explanation of the trigger source." }, - "initialSubscriptions": { + "events": { "type": "array", "items": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/AutomationTriggerEventDefinition" }, - "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." + "description": "Events clients may select for this trigger type." }, - "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." + "configSchema": { + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } }, "required": [ - "channel", - "protocolVersions", - "clientId" + "type", + "title", + "events" ] }, - "ClientCapabilities": { + "AutomationSessionTemplate": { "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.", + "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": { - "mcpApps": { + "provider": { + "type": "string", + "description": "Provider id. Omit to use the host's default provider." + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." + }, + "config": { "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." + "description": "Session configuration values accepted by `createSession`, normally\nobtained from `resolveSessionConfig`." } } }, - "InitializeResult": { + "AutomationDefinition": { "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.", + "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": { - "protocolVersion": { + "title": { "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": "Human-readable automation name." }, - "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." + "message": { + "$ref": "#/$defs/Message", + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, - "snapshots": { - "type": "array", - "items": { - "$ref": "#/$defs/Snapshot" - }, - "description": "Snapshots for each `initialSubscriptions` URI" + "session": { + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, - "defaultDirectory": { - "$ref": "#/$defs/URI", - "description": "Suggested default directory for remote filesystem browsing" + "enabled": { + "type": "boolean", + "description": "Whether automatic triggers may create runs. Manual runs remain available\nwhenever {@link AutomationOperation.Run} is advertised." }, - "completionTriggerCharacters": { + "triggers": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/AutomationTrigger" }, - "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": "Automatic triggers. An empty list means manual-only." }, - "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." + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." } }, "required": [ - "protocolVersion", - "serverSeq", - "snapshots" + "title", + "message", + "session", + "enabled", + "triggers" ] }, - "PingParams": { + "AutomationRuntimeState": { "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.", + "description": "Host-resolved execution context that is useful to clients but is not part of\nthe editable definition.", "properties": { - "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "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": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + "description": "Opaque host-defined runtime metadata." } - }, - "required": [ - "channel" - ] + } }, - "ReconnectParams": { + "AutomationSummary": { "type": "object", - "description": "Re-establishes a dropped connection. The server replays missed actions or\nprovides fresh snapshots.", + "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": { - "channel": { + "resource": { + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation:` URI." + }, + "title": { "type": "string", - "enum": [ - "ahp-root://" - ] + "description": "Current {@link AutomationDefinition.title}." }, - "_meta": { - "type": "object", - "additionalProperties": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + "enabled": { + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." }, - "clientId": { + "triggerCount": { + "type": "number", + "description": "Number of automatic triggers in the current definition." + }, + "nextRunAt": { "type": "string", - "description": "Client identifier from the original connection" + "description": "Earliest schedule occurrence awaiting evaluation, as an ISO 8601 timestamp. It may be in the past while catch-up is pending." }, - "lastSeenServerSeq": { + "lastRun": { + "$ref": "#/$defs/AutomationRunSummary", + "description": "Most recent retained run, when any run exists." + }, + "revision": { "type": "number", - "description": "Last `serverSeq` the client received" + "description": "Monotonic definition revision used for optimistic concurrency." }, - "subscriptions": { + "operations": { "type": "array", "items": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/AutomationOperation" }, - "description": "URIs the client was subscribed to" - } - }, - "required": [ - "channel", - "clientId", - "lastSeenServerSeq", - "subscriptions" + "description": "Operations currently permitted for this automation." + }, + "createdAt": { + "type": "string", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" ] }, - "ReconnectReplayResult": { + "AutomationState": { "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.", + "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": { - "type": { - "const": "replay", - "description": "Discriminant" + "resource": { + "$ref": "#/$defs/URI", + "description": "URI of this automation channel." }, - "actions": { + "definition": { + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." + }, + "revision": { + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." + }, + "nextRunAt": { + "type": "string", + "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", "items": { - "$ref": "#/$defs/ActionEnvelope" + "$ref": "#/$defs/AutomationRunSummary" }, - "description": "Missed action envelopes since `lastSeenServerSeq`" + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." }, - "missing": { + "runsNextCursor": { + "type": "string", + "description": "Opaque cursor for the next older run-history page." + }, + "runtime": { + "$ref": "#/$defs/AutomationRuntimeState", + "description": "Optional host-resolved execution context." + }, + "operations": { "type": "array", "items": { - "$ref": "#/$defs/URI" + "$ref": "#/$defs/AutomationOperation" }, - "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." + "description": "Operations currently permitted for this automation." + }, + "createdAt": { + "type": "string", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." } }, "required": [ - "type", - "actions", - "missing" + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" ] }, - "ReconnectSnapshotResult": { + "AutomationRunBlocker": { "type": "object", - "description": "Reconnect result when the gap exceeds the replay buffer.", + "description": "Summary of why a run cannot currently make progress.", "properties": { - "type": { - "const": "snapshot", - "description": "Discriminant" + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "description": "Cause recorded for a client-requested manual run.", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", + "properties": { + "kind": { + "const": "trigger" }, - "snapshots": { - "type": "array", - "items": { - "$ref": "#/$defs/Snapshot" - }, - "description": "Fresh snapshots for each subscription" + "triggerId": { + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." + }, + "scheduledFor": { + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." + }, + "catchUp": { + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "description": "A durable run exists but has not begun external execution.", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "description": "The run is actively executing linked sessions.", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "description": "The run started but is temporarily unable to progress.", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "completedAt": { + "type": "string", + "description": "Completion timestamp in ISO 8601 format." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Failure timestamp in ISO 8601 format." + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "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", + "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", + "description": "Stable artifact id within this run, used by artifact actions." + }, + "label": { + "type": "string", + "description": "Human-readable label suitable for run-history UI." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "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", + "description": "Subscribable `ahp-automation-run:` URI." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." + }, + "primarySession": { + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." + }, + "sessionCount": { + "type": "number", + "description": "Number of linked sessions, including attempts and workers." + }, + "artifactCount": { + "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": {}, + "description": "Opaque host-defined summary metadata." + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "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", + "description": "URI of this automation-run channel." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$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", + "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": {}, + "description": "Opaque host-defined run metadata." + } + }, + "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-owned automation support. Absence means the host does not expose an\nautomation catalogue or automation commands." + } + }, + "required": [ + "protocolVersion", + "serverSeq", + "snapshots" + ] + }, + "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", + "description": "Availability guarantee for automatic trigger execution." + }, + "create": { + "$ref": "#/$defs/AutomationCreateCapability", + "description": "Present when clients may call `createAutomation`." + }, + "schedules": { + "$ref": "#/$defs/AutomationScheduleCapabilities", + "description": "Present when definitions may contain schedule triggers." + }, + "runCancellation": { + "$ref": "#/$defs/AutomationRunCancellationCapability", + "description": "Present when clients may request cancellation on eligible runs." + }, + "schedulePreview": { + "$ref": "#/$defs/AutomationSchedulePreviewCapability", + "description": "Present when clients may call `previewAutomationSchedule`." + }, + "runHistoryLimit": { + "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": [ + "execution" + ] + }, + "AutomationExecutionCapabilities": { + "type": "object", + "description": "Automatic trigger execution availability.", + "properties": { + "lifetime": { + "$ref": "#/$defs/AutomationExecutionLifetime", + "description": "How long automatic trigger evaluation remains available." + } + }, + "required": [ + "lifetime" + ] + }, + "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": { + "minIntervalMinutes": { + "type": "number", + "description": "Smallest permitted interval between consecutive occurrences. Omission\nmeans no restriction beyond the cron format's one-minute resolution." + } + } + }, + "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": { + "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" + ] + }, + "ReconnectSnapshotResult": { + "type": "object", + "description": "Reconnect result when the gap exceeds the replay buffer.", + "properties": { + "type": { + "const": "snapshot", + "description": "Discriminant" + }, + "snapshots": { + "type": "array", + "items": { + "$ref": "#/$defs/Snapshot" + }, + "description": "Fresh snapshots for each subscription" + } + }, + "required": [ + "type", + "snapshots" + ] + }, + "SubscribeParams": { + "type": "object", + "description": "Subscribe to a URI-identified channel.\n\nA channel MAY have state associated with it (e.g. root, sessions,\nterminals) or be stateless (pure pub/sub for streaming data). For\nstate-bearing channels the result includes a snapshot; for stateless\nchannels `snapshot` is omitted.", + "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." + }, + "delivery": { + "$ref": "#/$defs/SubscriptionDeliveryOptions", + "description": "Optional delivery preferences for this subscription.\n\nServers MAY use these preferences to buffer and coalesce high-frequency\nupdates while preserving the same reduced state. Omit this field for the\nserver's default delivery behavior." + }, + "view": { + "$ref": "#/$defs/SubscribeView", + "description": "Optional client-requested shape for the returned snapshot.\n\nServers that do not understand a requested view ignore it and return their\ndefault snapshot. Clients MUST tolerate receiving more state than requested." + } + }, + "required": [ + "channel" + ] + }, + "SubscribeView": { + "type": "object", + "description": "Optional client-requested shape for a subscription snapshot.", + "properties": { + "turns": { + "type": "number", + "description": "Advisory number of most-recent completed turns to expose in a chat\nsnapshot.\n\nServers MAY return more or fewer turns than requested. When omitted, the\nhost MUST return all retained turns. When older turns remain available, the\nreturned {@link ChatState} carries `turnsNextCursor`; clients pass that\ncursor to `fetchTurns` to ask the host to page more turns into the chat\nstate." + } + } + }, + "SubscriptionDeliveryOptions": { + "type": "object", + "description": "Advisory delivery preferences for a single subscription.", + "properties": { + "maxLatencyMs": { + "type": "number", + "description": "Maximum time, in milliseconds, that the server may intentionally delay\ndelivery while buffering/coalescing updates for this subscription.\n\nA value of `0` requests immediate delivery with no intentional coalescing." + } + } + }, + "SubscribeResult": { + "type": "object", + "description": "Result of the `subscribe` command.\n\n`snapshot` is present when the subscribed channel has associated state, and\nabsent for stateless channels.", + "properties": { + "snapshot": { + "$ref": "#/$defs/Snapshot", + "description": "Snapshot of the subscribed channel's state (omitted for stateless channels)" + } + } + }, + "UnsubscribeParams": { + "type": "object", + "description": "Stop receiving updates for a channel.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI to unsubscribe from" + } + }, + "required": [ + "channel" + ] + }, + "DispatchActionParams": { + "type": "object", + "description": "Fire-and-forget action dispatch (write-ahead). The client applies actions\noptimistically to local state and the server echoes them back as an\n{@link ActionEnvelope} once accepted.\n\nThe client → server method is named `dispatchAction`; the server's reply\narrives on the server → client `action` notification (params:\n{@link ActionEnvelope}).", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this action targets" + }, + "clientSeq": { + "type": "number", + "description": "Client sequence number" + }, + "action": { + "$ref": "#/$defs/StateAction", + "description": "The action to dispatch" + } + }, + "required": [ + "channel", + "clientSeq", + "action" + ] + }, + "ResourceReadParams": { + "type": "object", + "description": "Reads the content of a resource by URI.\n\nContent references keep the state tree small by storing large data (images,\nlong tool outputs) by reference rather than inline.\n\nBinary content (images, etc.) MUST use `base64` encoding. Text content MAY\nuse `utf-8` encoding.\n\nLike all `resource*` methods, `resourceRead` is symmetrical and MAY be\nsent in either direction. Hosts use it to fetch content from a\nclient-published URI (e.g. `virtual://my-client/...` plugins); clients\nuse it to read host-side files. The receiver enforces access via the\nsame permission/`resourceRequest` flow regardless of which peer initiated.", + "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." + }, + "uri": { + "type": "string", + "description": "Content URI from a `ContentRef`" + }, + "encoding": { + "$ref": "#/$defs/ContentEncoding", + "description": "Preferred encoding for the returned data (default: server-chosen)" + } + }, + "required": [ + "channel", + "uri" + ] + }, + "ResourceReadResult": { + "type": "object", + "description": "Result of the `resourceRead` command.\n\nThe server SHOULD honor the `encoding` requested in the params. If the\nserver cannot provide the requested encoding, it MUST fall back to either\n`base64` or `utf-8`.", + "properties": { + "data": { + "type": "string", + "description": "Content encoded as a string" + }, + "encoding": { + "$ref": "#/$defs/ContentEncoding", + "description": "How `data` is encoded" + }, + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"image/png\"`, `\"text/plain\"`)" + } + }, + "required": [ + "data", + "encoding" + ] + }, + "ResourceWriteParams": { + "type": "object", + "description": "Writes content to a file on the server's filesystem.\n\nBinary content (images, etc.) MUST use `base64` encoding. Text content MAY\nuse `utf-8` encoding.\n\nIf the file does not exist, it is created. If the file already exists, the\neffect on existing bytes depends on {@link ResourceWriteParams.mode}:\n`truncate` (default) overwrites from the chosen offset onward, `append`\npreserves all existing bytes and adds `data` at a position rooted at EOF,\nand `insert` preserves all existing bytes and splices `data` in at an\noffset rooted at the start of the file.\n\nLike all `resource*` methods, `resourceWrite` is symmetrical and MAY be\nsent in either direction.", + "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." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Target file URI on the server filesystem" + }, + "data": { + "type": "string", + "description": "Content encoded as a string" + }, + "encoding": { + "$ref": "#/$defs/ContentEncoding", + "description": "How `data` is encoded" + }, + "contentType": { + "type": "string", + "description": "Content type (e.g. `\"text/plain\"`, `\"image/png\"`)" + }, + "createOnly": { + "type": "boolean", + "description": "If `true`, the server MUST fail if the file already exists instead of\noverwriting it. Useful for safe creation of new files." + }, + "mode": { + "$ref": "#/$defs/ResourceWriteMode", + "description": "How `data` is placed within the target file. Defaults to `'truncate'`\n(full overwrite) when omitted. See {@link ResourceWriteMode} for the\nmeaning of each mode and how it interprets {@link position}." + }, + "position": { + "type": "number", + "description": "Byte offset interpreted according to {@link mode}. Defaults to `0`.\n- `truncate`: offset from the start of the file at which to truncate\n before writing.\n- `append`: bytes back from EOF at which to insert `data`.\n- `insert`: offset from the start of the file at which to splice in\n `data`." + }, + "ifMatch": { + "type": "string", + "description": "Optimistic-concurrency token previously returned by\n{@link ResourceResolveResult.etag}. When set, the server MUST fail with\n`Conflict` if the current `etag` does not match — preventing lost\nupdates between a `resourceResolve` and a subsequent `resourceWrite`." + } + }, + "required": [ + "channel", + "uri", + "data", + "encoding" + ] + }, + "ResourceWriteResult": { + "type": "object", + "description": "Result of the `resourceWrite` command.\n\nAn empty object on success.", + "properties": {} + }, + "ResourceListParams": { + "type": "object", + "description": "Lists directory entries at a file URI on the server's filesystem.\n\nThis is intended for remote folder pickers and similar UI that needs to let\nusers navigate the server's local filesystem.\n\nThe server MUST return success only if the target exists and is a directory.\nIf the target does not exist, is not a directory, or cannot be accessed, the\nserver MUST return a JSON-RPC error.\n\nLike all `resource*` methods, `resourceList` is symmetrical and MAY be\nsent in either direction.", + "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." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Directory URI on the server filesystem" + } + }, + "required": [ + "channel", + "uri" + ] + }, + "DirectoryEntry": { + "type": "object", + "description": "Directory entry returned by `resourceList`.", + "properties": { + "name": { + "type": "string", + "description": "Base name of the entry" + }, + "type": { + "type": "string", + "enum": [ + "file", + "directory" + ], + "description": "Whether the entry is a file or directory" + } + }, + "required": [ + "name", + "type" + ] + }, + "ResourceListResult": { + "type": "object", + "description": "Result of the `resourceList` command.", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/$defs/DirectoryEntry" + }, + "description": "Entries directly contained in the requested directory" + } + }, + "required": [ + "entries" + ] + }, + "ResourceCopyParams": { + "type": "object", + "description": "Copies a resource from one URI to another on the server's filesystem.\n\nIf the destination already exists, it is overwritten unless `failIfExists`\nis set.\n\nLike all `resource*` methods, `resourceCopy` is symmetrical and MAY be\nsent in either direction.", + "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." + }, + "source": { + "$ref": "#/$defs/URI", + "description": "Source URI to copy from" + }, + "destination": { + "$ref": "#/$defs/URI", + "description": "Destination URI to copy to" + }, + "failIfExists": { + "type": "boolean", + "description": "If `true`, the server MUST fail if the destination already exists instead\nof overwriting it." } }, "required": [ - "type", - "snapshots" + "channel", + "source", + "destination" ] }, - "SubscribeParams": { + "ResourceCopyResult": { "type": "object", - "description": "Subscribe to a URI-identified channel.\n\nA channel MAY have state associated with it (e.g. root, sessions,\nterminals) or be stateless (pure pub/sub for streaming data). For\nstate-bearing channels the result includes a snapshot; for stateless\nchannels `snapshot` is omitted.", + "description": "Result of the `resourceCopy` command.\n\nAn empty object on success.", + "properties": {} + }, + "ResourceDeleteParams": { + "type": "object", + "description": "Deletes a resource at a URI on the server's filesystem.\n\nLike all `resource*` methods, `resourceDelete` is symmetrical and MAY be\nsent in either direction.", "properties": { "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." + "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." }, - "delivery": { - "$ref": "#/$defs/SubscriptionDeliveryOptions", - "description": "Optional delivery preferences for this subscription.\n\nServers MAY use these preferences to buffer and coalesce high-frequency\nupdates while preserving the same reduced state. Omit this field for the\nserver's default delivery behavior." + "uri": { + "$ref": "#/$defs/URI", + "description": "URI of the resource to delete" }, - "view": { - "$ref": "#/$defs/SubscribeView", - "description": "Optional client-requested shape for the returned snapshot.\n\nServers that do not understand a requested view ignore it and return their\ndefault snapshot. Clients MUST tolerate receiving more state than requested." + "recursive": { + "type": "boolean", + "description": "If `true` and the target is a directory, delete it and all its contents\nrecursively. If `false` (default), deleting a non-empty directory MUST fail." } }, "required": [ - "channel" + "channel", + "uri" ] }, - "SubscribeView": { - "type": "object", - "description": "Optional client-requested shape for a subscription snapshot.", - "properties": { - "turns": { - "type": "number", - "description": "Advisory number of most-recent completed turns to expose in a chat\nsnapshot.\n\nServers MAY return more or fewer turns than requested. When omitted, the\nhost MUST return all retained turns. When older turns remain available, the\nreturned {@link ChatState} carries `turnsNextCursor`; clients pass that\ncursor to `fetchTurns` to ask the host to page more turns into the chat\nstate." - } - } - }, - "SubscriptionDeliveryOptions": { - "type": "object", - "description": "Advisory delivery preferences for a single subscription.", - "properties": { - "maxLatencyMs": { - "type": "number", - "description": "Maximum time, in milliseconds, that the server may intentionally delay\ndelivery while buffering/coalescing updates for this subscription.\n\nA value of `0` requests immediate delivery with no intentional coalescing." - } - } - }, - "SubscribeResult": { + "ResourceDeleteResult": { "type": "object", - "description": "Result of the `subscribe` command.\n\n`snapshot` is present when the subscribed channel has associated state, and\nabsent for stateless channels.", - "properties": { - "snapshot": { - "$ref": "#/$defs/Snapshot", - "description": "Snapshot of the subscribed channel's state (omitted for stateless channels)" - } - } + "description": "Result of the `resourceDelete` command.\n\nAn empty object on success.", + "properties": {} }, - "UnsubscribeParams": { + "ResourceRequestParams": { "type": "object", - "description": "Stop receiving updates for a channel.", + "description": "Requests permission to access a resource on the receiver's filesystem.\n\n`resourceRequest` is symmetrical and MAY be sent in either direction: a\nclient asks the server to grant access to a server-side resource, or a\nserver asks the client to grant access to a client-side resource. The\nreceiver decides whether to allow, deny, or prompt the user for the\nrequested access.\n\nIf the receiver denies access, it MUST respond with `PermissionDenied`\n(-32009). The error data MAY include a `ResourceRequestParams` value\ndescribing the access the caller would need to be granted for the\noperation to succeed; see `PermissionDeniedErrorData` in\n`types/errors.ts`.\n\nAfter a successful `resourceRequest`, the caller MAY use the corresponding\n`resource*` commands (e.g. `resourceRead`, `resourceWrite`) to perform the\noperation. Receivers MAY rescind access at any time by returning\n`PermissionDenied` on subsequent operations.\n\nEither `read`, `write`, or both SHOULD be set to `true`. A request with\nneither flag set is treated as `read: true` by receivers.", "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." + }, + "uri": { "$ref": "#/$defs/URI", - "description": "Channel URI to unsubscribe from" + "description": "Resource URI being requested. Typically a `file:` URI on the receiver's\nfilesystem, but any URI scheme that the receiver mediates access to is\nallowed." + }, + "read": { + "type": "boolean", + "description": "Whether the caller needs read access to the resource." + }, + "write": { + "type": "boolean", + "description": "Whether the caller needs write access to the resource." } }, "required": [ - "channel" + "channel", + "uri" ] }, - "DispatchActionParams": { + "ResourceRequestResult": { "type": "object", - "description": "Fire-and-forget action dispatch (write-ahead). The client applies actions\noptimistically to local state and the server echoes them back as an\n{@link ActionEnvelope} once accepted.\n\nThe client → server method is named `dispatchAction`; the server's reply\narrives on the server → client `action` notification (params:\n{@link ActionEnvelope}).", + "description": "Result of the `resourceRequest` command.\n\nAn empty object on success.", + "properties": {} + }, + "ResourceMoveParams": { + "type": "object", + "description": "Moves (renames) a resource from one URI to another on the server's filesystem.\n\nIf the destination already exists, it is overwritten unless `failIfExists`\nis set.\n\nLike all `resource*` methods, `resourceMove` is symmetrical and MAY be\nsent in either direction.", "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." + }, + "source": { "$ref": "#/$defs/URI", - "description": "Channel URI this action targets" + "description": "Source URI to move from" }, - "clientSeq": { - "type": "number", - "description": "Client sequence number" + "destination": { + "$ref": "#/$defs/URI", + "description": "Destination URI to move to" }, - "action": { - "$ref": "#/$defs/StateAction", - "description": "The action to dispatch" + "failIfExists": { + "type": "boolean", + "description": "If `true`, the server MUST fail if the destination already exists instead\nof overwriting it." } }, "required": [ "channel", - "clientSeq", - "action" + "source", + "destination" ] }, - "ResourceReadParams": { + "ResourceMoveResult": { "type": "object", - "description": "Reads the content of a resource by URI.\n\nContent references keep the state tree small by storing large data (images,\nlong tool outputs) by reference rather than inline.\n\nBinary content (images, etc.) MUST use `base64` encoding. Text content MAY\nuse `utf-8` encoding.\n\nLike all `resource*` methods, `resourceRead` is symmetrical and MAY be\nsent in either direction. Hosts use it to fetch content from a\nclient-published URI (e.g. `virtual://my-client/...` plugins); clients\nuse it to read host-side files. The receiver enforces access via the\nsame permission/`resourceRequest` flow regardless of which peer initiated.", + "description": "Result of the `resourceMove` command.\n\nAn empty object on success.", + "properties": {} + }, + "ResourceResolveParams": { + "type": "object", + "description": "Resolves a resource — the combination of POSIX `stat` and `realpath`.\n\n`resourceResolve` returns metadata about the resource together with its\ncanonical URI after symlink resolution. Use this in place of any\n`resourceExists` shim: a missing resource MUST surface as a `NotFound`\nJSON-RPC error rather than a success with a sentinel value. Callers that\ntruly need a boolean check should attempt `resourceResolve` and treat\n`NotFound` as \"does not exist\".\n\nLike all `resource*` methods, `resourceResolve` is symmetrical and MAY be\nsent in either direction.", "properties": { "channel": { "type": "string", @@ -5385,12 +6542,12 @@ "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, "uri": { - "type": "string", - "description": "Content URI from a `ContentRef`" + "$ref": "#/$defs/URI", + "description": "URI to resolve" }, - "encoding": { - "$ref": "#/$defs/ContentEncoding", - "description": "Preferred encoding for the returned data (default: server-chosen)" + "followSymlinks": { + "type": "boolean", + "description": "When `true` (default), follow symlinks and report the metadata of the\nlink target — and set `uri` in the result to the canonical (realpath)\nURI. When `false`, stat the link itself (lstat semantics) and report\n`type: 'symlink'`." } }, "required": [ @@ -5398,31 +6555,47 @@ "uri" ] }, - "ResourceReadResult": { + "ResourceResolveResult": { "type": "object", - "description": "Result of the `resourceRead` command.\n\nThe server SHOULD honor the `encoding` requested in the params. If the\nserver cannot provide the requested encoding, it MUST fall back to either\n`base64` or `utf-8`.", + "description": "Result of the `resourceResolve` command.", "properties": { - "data": { + "uri": { + "$ref": "#/$defs/URI", + "description": "Canonical URI after symlink resolution. Equal to the requested URI when\n`followSymlinks` is `false` or the URI does not traverse a symlink." + }, + "type": { + "$ref": "#/$defs/ResourceType", + "description": "Resource kind." + }, + "size": { + "type": "number", + "description": "Size in bytes. Omitted for directories when the provider cannot\ncheaply compute it." + }, + "mtime": { "type": "string", - "description": "Content encoded as a string" + "description": "Last-modified time in ISO 8601 format, when known." }, - "encoding": { - "$ref": "#/$defs/ContentEncoding", - "description": "How `data` is encoded" + "ctime": { + "type": "string", + "description": "Creation time in ISO 8601 format, when known." }, "contentType": { "type": "string", - "description": "Content type (e.g. `\"image/png\"`, `\"text/plain\"`)" + "description": "Sniffed MIME type, when known (e.g. `\"text/plain\"`, `\"image/png\"`)." + }, + "etag": { + "type": "string", + "description": "Opaque per-provider version token. When present, pass it as\n{@link ResourceWriteParams.ifMatch} on a subsequent `resourceWrite` to\ndetect concurrent modifications." } }, "required": [ - "data", - "encoding" + "uri", + "type" ] }, - "ResourceWriteParams": { + "ResourceMkdirParams": { "type": "object", - "description": "Writes content to a file on the server's filesystem.\n\nBinary content (images, etc.) MUST use `base64` encoding. Text content MAY\nuse `utf-8` encoding.\n\nIf the file does not exist, it is created. If the file already exists, the\neffect on existing bytes depends on {@link ResourceWriteParams.mode}:\n`truncate` (default) overwrites from the chosen offset onward, `append`\npreserves all existing bytes and adds `data` at a position rooted at EOF,\nand `insert` preserves all existing bytes and splices `data` in at an\noffset rooted at the start of the file.\n\nLike all `resource*` methods, `resourceWrite` is symmetrical and MAY be\nsent in either direction.", + "description": "Creates a directory on the server's filesystem with `mkdir -p` semantics.\n\nThe server MUST create any missing parent directories. Creating a\ndirectory that already exists is a no-op success. If `uri` already\nexists but is **not** a directory, the server MUST fail with\n`AlreadyExists`.\n\nLike all `resource*` methods, `resourceMkdir` is symmetrical and MAY be\nsent in either direction.", "properties": { "channel": { "type": "string", @@ -5437,52 +6610,64 @@ }, "uri": { "$ref": "#/$defs/URI", - "description": "Target file URI on the server filesystem" - }, - "data": { + "description": "Directory URI to create (parents created as needed)." + } + }, + "required": [ + "channel", + "uri" + ] + }, + "ResourceMkdirResult": { + "type": "object", + "description": "Result of the `resourceMkdir` command.\n\nAn empty object on success.", + "properties": {} + }, + "AuthenticateParams": { + "type": "object", + "description": "Pushes a Bearer token for a protected resource. The `resource` field MUST\nmatch a protected-resource identifier the client has discovered from the\nserver — whether declared statically in `AgentInfo.protectedResources`,\nor discovered dynamically from a live `McpServerAuthRequiredState.resource`\nor `ToolCallAuthRequiredState.auth.resource` (both surfaced only once the\ncorresponding MCP server or tool call actually challenges for auth).\nServers MUST accept any `resource` value they have themselves advertised\nthrough one of these three mechanisms.\n\nTokens are delivered using [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)\n(Bearer Token Usage) semantics. The client obtains the token from the\nauthorization server(s) listed in the resource's metadata and pushes it\nto the server via this command.", + "properties": { + "channel": { "type": "string", - "description": "Content encoded as a string" + "enum": [ + "ahp-root://" + ] }, - "encoding": { - "$ref": "#/$defs/ContentEncoding", - "description": "How `data` is encoded" + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "contentType": { + "resource": { "type": "string", - "description": "Content type (e.g. `\"text/plain\"`, `\"image/png\"`)" - }, - "createOnly": { - "type": "boolean", - "description": "If `true`, the server MUST fail if the file already exists instead of\noverwriting it. Useful for safe creation of new files." - }, - "mode": { - "$ref": "#/$defs/ResourceWriteMode", - "description": "How `data` is placed within the target file. Defaults to `'truncate'`\n(full overwrite) when omitted. See {@link ResourceWriteMode} for the\nmeaning of each mode and how it interprets {@link position}." - }, - "position": { - "type": "number", - "description": "Byte offset interpreted according to {@link mode}. Defaults to `0`.\n- `truncate`: offset from the start of the file at which to truncate\n before writing.\n- `append`: bytes back from EOF at which to insert `data`.\n- `insert`: offset from the start of the file at which to splice in\n `data`." + "description": "The protected resource identifier. MUST match a `resource` value the\nserver has advertised — via `ProtectedResourceMetadata` in\n`AgentInfo.protectedResources`, or via a live\n`McpServerAuthRequiredState.resource` / `ToolCallAuthRequiredState.auth.resource`." }, - "ifMatch": { + "token": { "type": "string", - "description": "Optimistic-concurrency token previously returned by\n{@link ResourceResolveResult.etag}. When set, the server MUST fail with\n`Conflict` if the current `etag` does not match — preventing lost\nupdates between a `resourceResolve` and a subsequent `resourceWrite`." + "description": "Bearer token obtained from the resource's authorization server" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OAuth scopes the token grants, when known. Lets the server determine\nwhether a specific challenge — e.g. the `requiredScopes` on a live\n`McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth` — is\nsatisfied without decoding the (opaque, server-specific) token itself.\nOmit when the client doesn't track granted scopes separately from the\ntoken." } }, "required": [ "channel", - "uri", - "data", - "encoding" + "resource", + "token" ] }, - "ResourceWriteResult": { + "AuthenticateResult": { "type": "object", - "description": "Result of the `resourceWrite` command.\n\nAn empty object on success.", + "description": "Result of the `authenticate` command.\n\nAn empty object on success. If the token is invalid or the resource is\nunrecognized, the server MUST return a JSON-RPC error (e.g. `AuthRequired`\n`-32007` or `InvalidParams` `-32602`).", "properties": {} }, - "ResourceListParams": { + "ListSessionsParams": { "type": "object", - "description": "Lists directory entries at a file URI on the server's filesystem.\n\nThis is intended for remote folder pickers and similar UI that needs to let\nusers navigate the server's local filesystem.\n\nThe server MUST return success only if the target exists and is a directory.\nIf the target does not exist, is not a directory, or cannot be accessed, the\nserver MUST return a JSON-RPC error.\n\nLike all `resource*` methods, `resourceList` is symmetrical and MAY be\nsent in either direction.", + "description": "Returns a list of session summaries. Used to populate session lists and sidebars.\n\nThe session list is **not** part of the state tree because it can be arbitrarily\nlarge. Clients fetch it imperatively and maintain a local cache updated by\n`root/sessionAdded` and `root/sessionRemoved` notifications.\n\nA large catalogue can be fetched incrementally via the {@link PaginatedParams}\n`limit`/`cursor` inputs (see that type for the full pagination contract). The\nserver SHOULD return most-recently-modified entries first, so the first page\nis the immediately useful one. The `root/session*` notifications keep an\nalready-fetched page live; pagination governs only the initial and backfill\nfetches.", "properties": { "channel": { "type": "string", @@ -5495,57 +6680,42 @@ "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Directory URI on the server filesystem" - } - }, - "required": [ - "channel", - "uri" - ] - }, - "DirectoryEntry": { - "type": "object", - "description": "Directory entry returned by `resourceList`.", - "properties": { - "name": { - "type": "string", - "description": "Base name of the entry" + "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." }, - "type": { + "cursor": { "type": "string", - "enum": [ - "file", - "directory" - ], - "description": "Whether the entry is a file or directory" + "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." } }, "required": [ - "name", - "type" + "channel" ] }, - "ResourceListResult": { + "ListSessionsResult": { "type": "object", - "description": "Result of the `resourceList` command.", + "description": "Result of the `listSessions` command.", "properties": { - "entries": { + "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/DirectoryEntry" + "$ref": "#/$defs/SessionSummary" }, - "description": "Entries directly contained in the requested directory" + "description": "The list of session summaries. The server SHOULD order them\nmost-recently-modified first." } }, "required": [ - "entries" + "items" ] }, - "ResourceCopyParams": { + "ResolveSessionConfigParams": { "type": "object", - "description": "Copies a resource from one URI to another on the server's filesystem.\n\nIf the destination already exists, it is overwritten unless `failIfExists`\nis set.\n\nLike all `resource*` methods, `resourceCopy` is symmetrical and MAY be\nsent in either direction.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.", "properties": { "channel": { "type": "string", @@ -5558,33 +6728,68 @@ "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "source": { - "$ref": "#/$defs/URI", - "description": "Source URI to copy from" + "provider": { + "type": "string", + "description": "Agent provider ID" }, - "destination": { + "workingDirectory": { "$ref": "#/$defs/URI", - "description": "Destination URI to copy to" + "description": "Working directory for the session" }, - "failIfExists": { - "type": "boolean", - "description": "If `true`, the server MUST fail if the destination already exists instead\nof overwriting it." + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Current user-filled configuration values" } }, "required": [ - "channel", - "source", - "destination" + "channel" ] }, - "ResourceCopyResult": { + "ResolveSessionConfigResult": { "type": "object", - "description": "Result of the `resourceCopy` command.\n\nAn empty object on success.", - "properties": {} + "description": "Result of the `resolveSessionConfig` command.", + "properties": { + "schema": { + "$ref": "#/$defs/SessionConfigSchema", + "description": "JSON Schema describing available configuration properties given the current context" + }, + "values": { + "type": "object", + "additionalProperties": {}, + "description": "Current configuration values (echoed back with server-resolved defaults applied)" + } + }, + "required": [ + "schema", + "values" + ] }, - "ResourceDeleteParams": { + "SessionConfigValueItem": { "type": "object", - "description": "Deletes a resource at a URI on the server's filesystem.\n\nLike all `resource*` methods, `resourceDelete` is symmetrical and MAY be\nsent in either direction.", + "description": "A single value item returned by `sessionConfigCompletions`.", + "properties": { + "value": { + "type": "string", + "description": "The value to store in config" + }, + "label": { + "type": "string", + "description": "Human-readable display label" + }, + "description": { + "type": "string", + "description": "Optional secondary description" + } + }, + "required": [ + "value", + "label" + ] + }, + "SessionConfigCompletionsParams": { + "type": "object", + "description": "Queries the server for allowed values of a dynamic session config property.\n\nUsed when a property in the schema returned by `resolveSessionConfig` has\n`enumDynamic: true`. The client sends a search query and receives matching\nvalues with display metadata.", "properties": { "channel": { "type": "string", @@ -5597,752 +6802,770 @@ "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "uri": { + "provider": { + "type": "string", + "description": "Agent provider ID" + }, + "workingDirectory": { "$ref": "#/$defs/URI", - "description": "URI of the resource to delete" + "description": "Working directory for the session" }, - "recursive": { - "type": "boolean", - "description": "If `true` and the target is a directory, delete it and all its contents\nrecursively. If `false` (default), deleting a non-empty directory MUST fail." + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Current user-filled configuration values (provides context for the query)" + }, + "property": { + "type": "string", + "description": "Property id from the schema to query values for" + }, + "query": { + "type": "string", + "description": "Search filter text (empty or omitted returns default/recent values)" } }, "required": [ "channel", - "uri" + "property" ] }, - "ResourceDeleteResult": { + "SessionConfigCompletionsResult": { "type": "object", - "description": "Result of the `resourceDelete` command.\n\nAn empty object on success.", - "properties": {} + "description": "Result of the `sessionConfigCompletions` command.", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/SessionConfigValueItem" + }, + "description": "Matching value items" + } + }, + "required": [ + "items" + ] }, - "ResourceRequestParams": { + "SessionForkSource": { "type": "object", - "description": "Requests permission to access a resource on the receiver's filesystem.\n\n`resourceRequest` is symmetrical and MAY be sent in either direction: a\nclient asks the server to grant access to a server-side resource, or a\nserver asks the client to grant access to a client-side resource. The\nreceiver decides whether to allow, deny, or prompt the user for the\nrequested access.\n\nIf the receiver denies access, it MUST respond with `PermissionDenied`\n(-32009). The error data MAY include a `ResourceRequestParams` value\ndescribing the access the caller would need to be granted for the\noperation to succeed; see `PermissionDeniedErrorData` in\n`types/errors.ts`.\n\nAfter a successful `resourceRequest`, the caller MAY use the corresponding\n`resource*` commands (e.g. `resourceRead`, `resourceWrite`) to perform the\noperation. Receivers MAY rescind access at any time by returning\n`PermissionDenied` on subsequent operations.\n\nEither `read`, `write`, or both SHOULD be set to `true`. A request with\nneither flag set is treated as `read: true` by receivers.", + "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.", "properties": { - "channel": { + "session": { + "$ref": "#/$defs/URI", + "description": "URI of the existing session to fork from" + }, + "turnId": { "type": "string", - "enum": [ - "ahp-root://" - ] + "description": "Turn ID in the source session; content up to and including this turn's response is copied" + } + }, + "required": [ + "session", + "turnId" + ] + }, + "CreateSessionParams": { + "type": "object", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Session URI (client-chosen, e.g. `ahp-session:/`)" }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "Resource URI being requested. Typically a `file:` URI on the receiver's\nfilesystem, but any URI scheme that the receiver mediates access to is\nallowed." + "provider": { + "type": "string", + "description": "Agent provider ID" }, - "read": { - "type": "boolean", - "description": "Whether the caller needs read access to the resource." + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." }, - "write": { - "type": "boolean", - "description": "Whether the caller needs write access to the resource." + "fork": { + "$ref": "#/$defs/SessionForkSource", + "description": "Fork from an existing session. The new session is populated with content\nfrom the source session up to and including the specified turn's response." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server." + }, + "activeClient": { + "$ref": "#/$defs/SessionActiveClient", + "description": "Eagerly claim an active client role for the new session.\n\nWhen provided, the server initializes the session with this client as an\nactive client, equivalent to dispatching a `session/activeClientSet`\naction immediately after creation. The `clientId` MUST match the\n`clientId` the creating client supplied in `initialize`." + }, + "progressToken": { + "type": "string", + "description": "Opt-in progress token. When set, the client is offering to receive\n`progress` notifications (see `ProgressParams`) for any long-running work\nthe server does to bring this session up — most notably the lazy,\nfirst-use download of the provider's native SDK. The server echoes this\nexact token on every `progress` frame so the client can correlate it to\nthis `createSession` call (and the UI awaiting it).\n\nThe token MUST be unique across the client's active requests. The server\nMAY ignore it (e.g. when nothing long-running is needed), in which case no\n`progress` notifications are emitted." } }, "required": [ - "channel", - "uri" + "channel" ] }, - "ResourceRequestResult": { + "DisposeSessionParams": { "type": "object", - "description": "Result of the `resourceRequest` command.\n\nAn empty object on success.", - "properties": {} + "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.", + "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" + ] }, - "ResourceMoveParams": { + "FetchTurnsParams": { "type": "object", - "description": "Moves (renames) a resource from one URI to another on the server's filesystem.\n\nIf the destination already exists, it is overwritten unless `failIfExists`\nis set.\n\nLike all `resource*` methods, `resourceMove` is symmetrical and MAY be\nsent in either direction.", + "description": "Requests that the host load older historical turns into a chat state.\n\nThe command result does not carry turns. Instead, before responding, the host\nMUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat\nchannel's `turns` state, ahead of the already-loaded window, and update or\nclear `turnsNextCursor`.\n\nBefore applying any operation that references a turn outside the currently\nloaded window, the host MUST eagerly load enough older turns into state for\nthat operation to reduce against valid state.", "properties": { "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "$ref": "#/$defs/URI", + "description": "Chat URI" }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "source": { - "$ref": "#/$defs/URI", - "description": "Source URI to move from" - }, - "destination": { - "$ref": "#/$defs/URI", - "description": "Destination URI to move to" - }, - "failIfExists": { - "type": "boolean", - "description": "If `true`, the server MUST fail if the destination already exists instead\nof overwriting it." + "cursor": { + "type": "string", + "description": "Opaque cursor from `ChatState.turnsNextCursor`.\n\nThe host MUST reject unrecognised cursors with `InvalidParams`. Omit only\nwhen asking the host to opportunistically load its next older page for the\nchat, if any." } }, "required": [ - "channel", - "source", - "destination" + "channel" ] }, - "ResourceMoveResult": { + "FetchTurnsResult": { "type": "object", - "description": "Result of the `resourceMove` command.\n\nAn empty object on success.", + "description": "Result of the `fetchTurns` command.", "properties": {} }, - "ResourceResolveParams": { + "CompletionsParams": { "type": "object", - "description": "Resolves a resource — the combination of POSIX `stat` and `realpath`.\n\n`resourceResolve` returns metadata about the resource together with its\ncanonical URI after symlink resolution. Use this in place of any\n`resourceExists` shim: a missing resource MUST surface as a `NotFound`\nJSON-RPC error rather than a success with a sentinel value. Callers that\ntruly need a boolean check should attempt `resourceResolve` and treat\n`NotFound` as \"does not exist\".\n\nLike all `resource*` methods, `resourceResolve` is symmetrical and MAY be\nsent in either direction.", + "description": "Requests completion items for a partially-typed input (e.g. a user message\nthe user is currently composing). Used to power `@`-mention pickers,\nfile/symbol references, and similar inline-completion experiences.\n\nServers SHOULD treat this command as best-effort and return promptly. The\nclient SHOULD debounce calls to avoid flooding the server with requests on\nevery keystroke.", "properties": { "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "$ref": "#/$defs/URI", + "description": "The chat URI the completion is being requested for." }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "URI to resolve" + "kind": { + "$ref": "#/$defs/CompletionItemKind", + "description": "What kind of completion is being requested." }, - "followSymlinks": { - "type": "boolean", - "description": "When `true` (default), follow symlinks and report the metadata of the\nlink target — and set `uri` in the result to the canonical (realpath)\nURI. When `false`, stat the link itself (lstat semantics) and report\n`type: 'symlink'`." + "text": { + "type": "string", + "description": "The complete text of the input being completed (e.g. the full user\nmessage text typed so far)." + }, + "offset": { + "type": "number", + "description": "The character offset within `text` at which the completion is requested,\nmeasured in UTF-16 code units. MUST satisfy `0 <= offset <= text.length`." } }, "required": [ "channel", - "uri" + "kind", + "text", + "offset" ] }, - "ResourceResolveResult": { + "CompletionItem": { "type": "object", - "description": "Result of the `resourceResolve` command.", + "description": "A single completion item returned by the `completions` command.\n\nWhen the user accepts an item, the client SHOULD:\n1. Replace the range `[rangeStart, rangeEnd)` in the input with `insertText`\n (or insert `insertText` at the cursor when the range is omitted).\n2. Associate the item's `attachment` with the resulting {@link Message}.", "properties": { - "uri": { - "$ref": "#/$defs/URI", - "description": "Canonical URI after symlink resolution. Equal to the requested URI when\n`followSymlinks` is `false` or the URI does not traverse a symlink." - }, - "type": { - "$ref": "#/$defs/ResourceType", - "description": "Resource kind." + "insertText": { + "type": "string", + "description": "The text inserted into the input when this item is accepted." }, - "size": { + "rangeStart": { "type": "number", - "description": "Size in bytes. Omitted for directories when the provider cannot\ncheaply compute it." + "description": "If defined, the start of the range in the input's `text` that is replaced\nby `insertText`. The range is the half-open interval\n`[rangeStart, rangeEnd)` of character offsets, measured in UTF-16 code\nunits.\n\nWhen omitted, the client SHOULD insert `insertText` at the cursor.\n\nNote: this range refers to positions in the *current* input. The\nattachment's own `rangeStart`/`rangeEnd` (when present) refer to\npositions in the final {@link Message.text} after the item is\naccepted." }, - "mtime": { - "type": "string", - "description": "Last-modified time in ISO 8601 format, when known." + "rangeEnd": { + "type": "number", + "description": "The end of the range in the input's `text` that is replaced by\n`insertText`. See {@link rangeStart}." }, - "ctime": { - "type": "string", - "description": "Creation time in ISO 8601 format, when known." + "attachment": { + "$ref": "#/$defs/MessageAttachment", + "description": "The attachment associated with this completion item." + } + }, + "required": [ + "insertText", + "attachment" + ] + }, + "CompletionsResult": { + "type": "object", + "description": "Result of the `completions` command.", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/CompletionItem" + }, + "description": "The completion items, in the order the server suggests displaying them." + } + }, + "required": [ + "items" + ] + }, + "ForkChatSource": { + "type": "object", + "description": "Copies source history through a completed turn into the new chat.", + "properties": { + "kind": { + "const": "fork", + "description": "Discriminant" }, - "contentType": { - "type": "string", - "description": "Sniffed MIME type, when known (e.g. `\"text/plain\"`, `\"image/png\"`)." + "chat": { + "$ref": "#/$defs/URI", + "description": "URI of the existing source chat." }, - "etag": { + "turnId": { "type": "string", - "description": "Opaque per-provider version token. When present, pass it as\n{@link ResourceWriteParams.ifMatch} on a subsequent `resourceWrite` to\ndetect concurrent modifications." + "description": "Completed turn identifier in the source chat.\n\nContent through this turn is copied into the new chat's visible `turns`." } }, "required": [ - "uri", - "type" + "kind", + "chat", + "turnId" ] }, - "ResourceMkdirParams": { + "SideChatSource": { "type": "object", - "description": "Creates a directory on the server's filesystem with `mkdir -p` semantics.\n\nThe server MUST create any missing parent directories. Creating a\ndirectory that already exists is a no-op success. If `uri` already\nexists but is **not** a directory, the server MUST fail with\n`AlreadyExists`.\n\nLike all `resource*` methods, `resourceMkdir` is symmetrical and MAY be\nsent in either direction.", + "description": "Supplies source context to a new side chat without copying it into the side\nchat's visible history.", "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." + "kind": { + "const": "sideChat", + "description": "Discriminant" }, - "uri": { + "chat": { "$ref": "#/$defs/URI", - "description": "Directory URI to create (parents created as needed)." + "description": "URI of the existing source chat." + }, + "turnId": { + "type": "string", + "description": "Stable source-turn identifier in the source chat.\n\nHosts resolve this id against the source chat's current `activeTurn` or its\nretained `turns` when accepting `createChat`. If it names the current\nactive turn, the host snapshots the source chat's retained history plus\nthat turn's current user message and any partial assistant response already\navailable. Once that turn later becomes historical, it is still referenced\nby this same identifier." + }, + "selection": { + "$ref": "#/$defs/SideChatSelection", + "description": "Optional immutable selected-text snapshot to carry into the created side\nchat's origin.\n\nWhen present, the host MUST snapshot and preserve this exact selection when\nit accepts `createChat`; later source-turn deltas do not alter it." } }, "required": [ - "channel", - "uri" + "kind", + "chat", + "turnId" ] }, - "ResourceMkdirResult": { - "type": "object", - "description": "Result of the `resourceMkdir` command.\n\nAn empty object on success.", - "properties": {} - }, - "AuthenticateParams": { + "CreateChatParams": { "type": "object", - "description": "Pushes a Bearer token for a protected resource. The `resource` field MUST\nmatch a protected-resource identifier the client has discovered from the\nserver — whether declared statically in `AgentInfo.protectedResources`,\nor discovered dynamically from a live `McpServerAuthRequiredState.resource`\nor `ToolCallAuthRequiredState.auth.resource` (both surfaced only once the\ncorresponding MCP server or tool call actually challenges for auth).\nServers MUST accept any `resource` value they have themselves advertised\nthrough one of these three mechanisms.\n\nTokens are delivered using [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)\n(Bearer Token Usage) semantics. The client obtains the token from the\nauthorization server(s) listed in the resource's metadata and pushes it\nto the server via this command.", + "description": "Creates a new chat within a session.", "properties": { "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "$ref": "#/$defs/URI", + "description": "Session URI containing the new chat." }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "resource": { - "type": "string", - "description": "The protected resource identifier. MUST match a `resource` value the\nserver has advertised — via `ProtectedResourceMetadata` in\n`AgentInfo.protectedResources`, or via a live\n`McpServerAuthRequiredState.resource` / `ToolCallAuthRequiredState.auth.resource`." + "chat": { + "$ref": "#/$defs/URI", + "description": "Chat URI (client-chosen, e.g. `ahp-chat:/`)." }, - "token": { - "type": "string", - "description": "Bearer token obtained from the resource's authorization server" + "initialMessage": { + "$ref": "#/$defs/Message", + "description": "Optional initial message for the new chat." }, - "scopes": { + "source": { + "$ref": "#/$defs/ChatSource", + "description": "Optional source chat and source turn.\n\nThe source chat MUST belong to this session. Clients MUST only request\n`kind: \"fork\"` when the selected agent advertises\n`capabilities.multipleChats.fork`, and `kind: \"sideChat\"` when the\nselected agent advertises `capabilities.multipleChats.sideChat`. Both\nsource forms carry a stable top-level `turnId`. Forks target completed\nturns. Side chats also carry a stable `turnId`, which the host resolves\nagainst the source chat's current active turn or retained history. If it\nresolves to the active turn, the host snapshots the currently available\npartial response when accepting `createChat`. When\n`source.kind === \"sideChat\"` and `source.selection` is present, the host\nalso snapshots and preserves that exact selected text in the created chat's\norigin; any `responsePartId` there is provenance only, not a live range." + }, + "workingDirectories": { "type": "array", "items": { - "type": "string" + "$ref": "#/$defs/URI" }, - "description": "OAuth scopes the token grants, when known. Lets the server determine\nwhether a specific challenge — e.g. the `requiredScopes` on a live\n`McpServerAuthRequiredState` or `ToolCallAuthRequiredState.auth` — is\nsatisfied without decoding the (opaque, server-specific) token itself.\nOmit when the client doesn't track granted scopes separately from the\ntoken." + "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." } }, "required": [ "channel", - "resource", - "token" + "chat" ] }, - "AuthenticateResult": { - "type": "object", - "description": "Result of the `authenticate` command.\n\nAn empty object on success. If the token is invalid or the resource is\nunrecognized, the server MUST return a JSON-RPC error (e.g. `AuthRequired`\n`-32007` or `InvalidParams` `-32602`).", - "properties": {} - }, - "ListSessionsParams": { + "DisposeChatParams": { "type": "object", - "description": "Returns a list of session summaries. Used to populate session lists and sidebars.\n\nThe session list is **not** part of the state tree because it can be arbitrarily\nlarge. Clients fetch it imperatively and maintain a local cache updated by\n`root/sessionAdded` and `root/sessionRemoved` notifications.\n\nA large catalogue can be fetched incrementally via the {@link PaginatedParams}\n`limit`/`cursor` inputs (see that type for the full pagination contract). The\nserver SHOULD return most-recently-modified entries first, so the first page\nis the immediately useful one. The `root/session*` notifications keep an\nalready-fetched page live; pagination governs only the initial and backfill\nfetches.", + "description": "Disposes a chat and cleans up server-side resources.", "properties": { "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "$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." - }, - "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." - } - }, - "required": [ - "channel" - ] - }, - "ListSessionsResult": { - "type": "object", - "description": "Result of the `listSessions` 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." - }, - "items": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionSummary" - }, - "description": "The list of session summaries. The server SHOULD order them\nmost-recently-modified first." } }, "required": [ - "items" + "channel" ] }, - "ResolveSessionConfigParams": { + "CreateTerminalParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.", + "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", "properties": { "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "$ref": "#/$defs/URI", + "description": "Terminal URI (client-chosen)." }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "provider": { + "claim": { + "$ref": "#/$defs/TerminalClaim", + "description": "Initial owner of the terminal" + }, + "name": { "type": "string", - "description": "Agent provider ID" + "description": "Human-readable terminal name" }, - "workingDirectory": { + "cwd": { "$ref": "#/$defs/URI", - "description": "Working directory for the session" + "description": "Initial working directory URI" }, - "config": { - "type": "object", - "additionalProperties": {}, - "description": "Current user-filled configuration values" + "cols": { + "type": "number", + "description": "Initial terminal width in columns" + }, + "rows": { + "type": "number", + "description": "Initial terminal height in rows" } }, "required": [ - "channel" + "channel", + "claim" ] }, - "ResolveSessionConfigResult": { + "DisposeTerminalParams": { "type": "object", - "description": "Result of the `resolveSessionConfig` command.", + "description": "Disposes a terminal and kills its process if still running.\n\nThe server dispatches `root/terminalsChanged` to remove the terminal from\nthe root terminal list.", "properties": { - "schema": { - "$ref": "#/$defs/SessionConfigSchema", - "description": "JSON Schema describing available configuration properties given the current context" + "channel": { + "$ref": "#/$defs/URI", + "description": "Channel URI this command targets." }, - "values": { + "_meta": { "type": "object", "additionalProperties": {}, - "description": "Current configuration values (echoed back with server-resolved defaults applied)" + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." } }, "required": [ - "schema", - "values" + "channel" ] }, - "SessionConfigValueItem": { + "ChangesetOperationFollowUp": { "type": "object", - "description": "A single value item returned by `sessionConfigCompletions`.", + "description": "Optional follow-up surfaced by the server after an operation completes —\na {@link ContentRef} the client can fetch and display.\n\nSet `external` to `true` to open the content in the user's preferred\nexternal handler (e.g. browser); otherwise the client is expected to\nsurface it inline.", "properties": { - "value": { - "type": "string", - "description": "The value to store in config" - }, - "label": { - "type": "string", - "description": "Human-readable display label" + "content": { + "$ref": "#/$defs/ContentRef" }, - "description": { - "type": "string", - "description": "Optional secondary description" + "external": { + "type": "boolean", + "description": "When `true`, open in an external handler rather than inline." } }, "required": [ - "value", - "label" + "content" ] }, - "SessionConfigCompletionsParams": { + "InvokeChangesetOperationParams": { "type": "object", - "description": "Queries the server for allowed values of a dynamic session config property.\n\nUsed when a property in the schema returned by `resolveSessionConfig` has\n`enumDynamic: true`. The client sends a search query and receives matching\nvalues with display metadata.", + "description": "Invokes a server-defined {@link ChangesetOperation} against a changeset,\na single file, or a line range.\n\nThe server validates that `operationId` exists in the changeset's\ncurrent `operations` list and that the requested `target.kind` is\ncontained in the operation's `scopes`. Invalid combinations result in a\nJSON-RPC error.\n\nState changes resulting from invocation flow back through the normal\n`changeset/*` action stream on the relevant changeset URIs. Clients\nSHOULD NOT synthesise local optimistic changes for invocations unless\nthe server explicitly opts in via a future capability.", "properties": { "channel": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "$ref": "#/$defs/URI", + "description": "The expanded changeset URI." }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "provider": { - "type": "string", - "description": "Agent provider ID" - }, - "workingDirectory": { - "$ref": "#/$defs/URI", - "description": "Working directory for the session" - }, - "config": { - "type": "object", - "additionalProperties": {}, - "description": "Current user-filled configuration values (provides context for the query)" - }, - "property": { + "operationId": { "type": "string", - "description": "Property id from the schema to query values for" + "description": "Matches {@link ChangesetOperation.id} from the changeset's `operations` list." }, - "query": { - "type": "string", - "description": "Search filter text (empty or omitted returns default/recent values)" + "target": { + "$ref": "#/$defs/ChangesetOperationTarget", + "description": "Target of the operation. Required iff the chosen scope is\n`'resource'` or `'range'`. Omit for changeset-scoped operations." } }, "required": [ "channel", - "property" - ] - }, - "SessionConfigCompletionsResult": { - "type": "object", - "description": "Result of the `sessionConfigCompletions` command.", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/$defs/SessionConfigValueItem" - }, - "description": "Matching value items" - } - }, - "required": [ - "items" + "operationId" ] }, - "SessionForkSource": { + "InvokeChangesetOperationResult": { "type": "object", - "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.", + "description": "Result of the {@link InvokeChangesetOperationParams | `invokeChangesetOperation`}\ncommand.\n\nSuccess is implicit: the server returns this result when it accepted\nthe operation. Failure is signalled by rejecting the JSON-RPC request\nwith an appropriate error code, not by any field on this result. The\noperation MAY still produce subsequent failure feedback through the\n{@link ChangesetStatusChangedAction | `changeset/statusChanged`} stream.", "properties": { - "session": { - "$ref": "#/$defs/URI", - "description": "URI of the existing session to fork from" + "message": { + "$ref": "#/$defs/StringOrMarkdown", + "description": "Optional human-readable message describing the result." }, - "turnId": { - "type": "string", - "description": "Turn ID in the source session; content up to and including this turn's response is copied" + "followUp": { + "$ref": "#/$defs/ChangesetOperationFollowUp", + "description": "Optional follow-up: a URI to open (e.g. a PR), a content ref, etc." } - }, - "required": [ - "session", - "turnId" - ] + } }, - "CreateSessionParams": { + "CreateResourceWatchParams": { "type": "object", + "description": "Creates a resource watcher on the receiver's filesystem.\n\nThe receiver allocates an `ahp-resource-watch:/` channel URI and\nreturns it on {@link CreateResourceWatchResult.channel}. The caller then\n[`subscribe`](./subscriptions)s to that channel to receive\n`resourceWatch/changed` actions over the standard action envelope.\n\nThe watch lifecycle is tied to subscription: when every subscriber has\nunsubscribed (or the underlying connection drops), the receiver MUST\nrelease the watcher. There is no explicit dispose command — `unsubscribe`\nis the only handle the caller needs.\n\nLike the rest of the `resource*` family, `createResourceWatch` is\nsymmetrical and MAY be sent in either direction. Access is gated through\nthe same permission flow as `resourceRead`/`resourceWrite`.", "properties": { "channel": { - "$ref": "#/$defs/URI", - "description": "Session URI (client-chosen, e.g. `ahp-session:/`)" + "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." }, - "provider": { - "type": "string", - "description": "Agent provider ID" - }, - "workingDirectories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} (in which case\nthe first entry is a fixed process root).\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch `session/workingDirectorySet` /\n`session/workingDirectoryRemoved` to change the set after the session has\nstarted.\n\nIgnored for forked sessions — a fork inherits its working directories\nfrom the source session identified by `fork`." + "uri": { + "$ref": "#/$defs/URI", + "description": "URI to watch." }, - "fork": { - "$ref": "#/$defs/SessionForkSource", - "description": "Fork from an existing session. The new session is populated with content\nfrom the source session up to and including the specified turn's response." + "recursive": { + "type": "boolean", + "description": "If `true`, the receiver MUST report changes for descendants of `uri`.\nIf `false` (default), only changes to `uri` itself — and, when `uri`\nis a directory, its direct children — are reported." }, - "config": { + "excludes": { "type": "object", - "additionalProperties": {}, - "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server." - }, - "activeClient": { - "$ref": "#/$defs/SessionActiveClient", - "description": "Eagerly claim an active client role for the new session.\n\nWhen provided, the server initializes the session with this client as an\nactive client, equivalent to dispatching a `session/activeClientSet`\naction immediately after creation. The `clientId` MUST match the\n`clientId` the creating client supplied in `initialize`." + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "items" + ], + "description": "Glob patterns or paths relative to `uri` to exclude from reporting.\nWrapped in `{ items }` for forward compatibility." }, - "progressToken": { - "type": "string", - "description": "Opt-in progress token. When set, the client is offering to receive\n`progress` notifications (see `ProgressParams`) for any long-running work\nthe server does to bring this session up — most notably the lazy,\nfirst-use download of the provider's native SDK. The server echoes this\nexact token on every `progress` frame so the client can correlate it to\nthis `createSession` call (and the UI awaiting it).\n\nThe token MUST be unique across the client's active requests. The server\nMAY ignore it (e.g. when nothing long-running is needed), in which case no\n`progress` notifications are emitted." + "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" + "channel", + "uri" ] }, - "DisposeSessionParams": { + "CreateResourceWatchResult": { "type": "object", - "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.", + "description": "Result of the `createResourceWatch` command.", "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." + "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" ] }, - "FetchTurnsParams": { + "ListAutomationsParams": { "type": "object", - "description": "Requests that the host load older historical turns into a chat state.\n\nThe command result does not carry turns. Instead, before responding, the host\nMUST dispatch `chat/turnsLoaded` to insert any loaded turns into the chat\nchannel's `turns` state, ahead of the already-loaded window, and update or\nclear `turnsNextCursor`.\n\nBefore applying any operation that references a turn outside the currently\nloaded window, the host MUST eagerly load enough older turns into state for\nthat operation to reduce against valid state.", + "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": { - "$ref": "#/$defs/URI", - "description": "Chat URI" + "type": "string", + "enum": [ + "ahp-root://" + ], + "description": "Automation catalogues are listed from the root channel." }, "_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." + }, "cursor": { "type": "string", - "description": "Opaque cursor from `ChatState.turnsNextCursor`.\n\nThe host MUST reject unrecognised cursors with `InvalidParams`. Omit only\nwhen asking the host to opportunistically load its next older page for the\nchat, if any." + "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", + "description": "Optional exact filter on {@link AutomationDefinition.enabled}." + } + }, + "required": [ + "channel" + ] + }, + "ListAutomationsResult": { + "type": "object", + "description": "One page of the automation catalogue.", + "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" + }, + "description": "Automation summaries in host-defined catalogue order." } }, "required": [ - "channel" + "items" ] }, - "FetchTurnsResult": { - "type": "object", - "description": "Result of the `fetchTurns` command.", - "properties": {} - }, - "CompletionsParams": { + "ListAutomationTriggerDefinitionsParams": { "type": "object", - "description": "Requests completion items for a partially-typed input (e.g. a user message\nthe user is currently composing). Used to power `@`-mention pickers,\nfile/symbol references, and similar inline-completion experiences.\n\nServers SHOULD treat this command as best-effort and return promptly. The\nclient SHOULD debounce calls to avoid flooding the server with requests on\nevery keystroke.", + "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": { - "$ref": "#/$defs/URI", - "description": "The chat URI the completion is being requested for." + "type": "string", + "enum": [ + "ahp-root://" + ], + "description": "Trigger definitions are discovered from the root channel." }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "kind": { - "$ref": "#/$defs/CompletionItemKind", - "description": "What kind of completion is being requested." - }, - "text": { - "type": "string", - "description": "The complete text of the input being completed (e.g. the full user\nmessage text typed so far)." - }, - "offset": { - "type": "number", - "description": "The character offset within `text` at which the completion is requested,\nmeasured in UTF-16 code units. MUST satisfy `0 <= offset <= text.length`." - } - }, - "required": [ - "channel", - "kind", - "text", - "offset" - ] - }, - "CompletionItem": { - "type": "object", - "description": "A single completion item returned by the `completions` command.\n\nWhen the user accepts an item, the client SHOULD:\n1. Replace the range `[rangeStart, rangeEnd)` in the input with `insertText`\n (or insert `insertText` at the cursor when the range is omitted).\n2. Associate the item's `attachment` with the resulting {@link Message}.", - "properties": { - "insertText": { + "provider": { "type": "string", - "description": "The text inserted into the input when this item is accepted." - }, - "rangeStart": { - "type": "number", - "description": "If defined, the start of the range in the input's `text` that is replaced\nby `insertText`. The range is the half-open interval\n`[rangeStart, rangeEnd)` of character offsets, measured in UTF-16 code\nunits.\n\nWhen omitted, the client SHOULD insert `insertText` at the cursor.\n\nNote: this range refers to positions in the *current* input. The\nattachment's own `rangeStart`/`rangeEnd` (when present) refer to\npositions in the final {@link Message.text} after the item is\naccepted." + "description": "Prospective provider id, or omitted for the host default." }, - "rangeEnd": { - "type": "number", - "description": "The end of the range in the input's `text` that is replaced by\n`insertText`. See {@link rangeStart}." + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Prospective ordered working-directory list." }, - "attachment": { - "$ref": "#/$defs/MessageAttachment", - "description": "The attachment associated with this completion item." + "sessionConfig": { + "type": "object", + "additionalProperties": {}, + "description": "Prospective resolved session configuration values." } }, "required": [ - "insertText", - "attachment" + "channel" ] }, - "CompletionsResult": { + "ListAutomationTriggerDefinitionsResult": { "type": "object", - "description": "Result of the `completions` command.", + "description": "Host-defined event trigger types available for the supplied context.", "properties": { "items": { "type": "array", "items": { - "$ref": "#/$defs/CompletionItem" + "$ref": "#/$defs/AutomationTriggerDefinition" }, - "description": "The completion items, in the order the server suggests displaying them." + "description": "Available event trigger definitions." } }, "required": [ "items" ] }, - "ForkChatSource": { + "AutomationImportTriggerNextRun": { "type": "object", - "description": "Copies source history through a completed turn into the new chat.", + "description": "Initial schedule occurrence retained while an imported automation is disabled.", "properties": { - "kind": { - "const": "fork", - "description": "Discriminant" - }, - "chat": { - "$ref": "#/$defs/URI", - "description": "URI of the existing source chat." + "triggerId": { + "type": "string", + "description": "Stable id of a schedule trigger in the imported definition." }, - "turnId": { + "nextRunAt": { "type": "string", - "description": "Completed turn identifier in the source chat.\n\nContent through this turn is copied into the new chat's visible `turns`." + "description": "Source scheduler's next unevaluated occurrence, as an ISO 8601 timestamp." } }, "required": [ - "kind", - "chat", - "turnId" + "triggerId", + "nextRunAt" ] }, - "SideChatSource": { + "AutomationImport": { "type": "object", - "description": "Supplies source context to a new side chat without copying it into the side\nchat's visible history.", + "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": { - "kind": { - "const": "sideChat", - "description": "Discriminant" + "source": { + "type": "string", + "description": "Stable namespace identifying the source implementation or store." }, - "chat": { - "$ref": "#/$defs/URI", - "description": "URI of the existing source chat." + "batchId": { + "type": "string", + "description": "Identifier shared by every item in one import attempt." }, - "turnId": { + "itemId": { "type": "string", - "description": "Stable source-turn identifier in the source chat.\n\nHosts resolve this id against the source chat's current `activeTurn` or its\nretained `turns` when accepting `createChat`. If it names the current\nactive turn, the host snapshots the source chat's retained history plus\nthat turn's current user message and any partial assistant response already\navailable. Once that turn later becomes historical, it is still referenced\nby this same identifier." + "description": "Stable source-side identifier for this definition within the batch." }, - "selection": { - "$ref": "#/$defs/SideChatSelection", - "description": "Optional immutable selected-text snapshot to carry into the created side\nchat's origin.\n\nWhen present, the host MUST snapshot and preserve this exact selection when\nit accepts `createChat`; later source-turn deltas do not alter it." + "triggerNextRuns": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationImportTriggerNextRun" + }, + "description": "Source schedule occurrences to retain until the imported definition is enabled." } }, "required": [ - "kind", - "chat", - "turnId" + "source", + "batchId", + "itemId" ] }, - "CreateChatParams": { + "CreateAutomationParams": { "type": "object", - "description": "Creates a new chat within a session.", + "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", - "description": "Session URI containing the new chat." + "description": "Client-chosen `ahp-automation:` URI for the new definition." }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "chat": { - "$ref": "#/$defs/URI", - "description": "Chat URI (client-chosen, e.g. `ahp-chat:/`)." - }, - "initialMessage": { - "$ref": "#/$defs/Message", - "description": "Optional initial message for the new chat." - }, - "source": { - "$ref": "#/$defs/ChatSource", - "description": "Optional source chat and source turn.\n\nThe source chat MUST belong to this session. Clients MUST only request\n`kind: \"fork\"` when the selected agent advertises\n`capabilities.multipleChats.fork`, and `kind: \"sideChat\"` when the\nselected agent advertises `capabilities.multipleChats.sideChat`. Both\nsource forms carry a stable top-level `turnId`. Forks target completed\nturns. Side chats also carry a stable `turnId`, which the host resolves\nagainst the source chat's current active turn or retained history. If it\nresolves to the active turn, the host snapshots the currently available\npartial response when accepting `createChat`. When\n`source.kind === \"sideChat\"` and `source.selection` is present, the host\nalso snapshots and preserves that exact selected text in the created chat's\norigin; any `responsePartId` there is provenance only, not a live range." + "definition": { + "$ref": "#/$defs/AutomationDefinition", + "description": "Complete initial definition." }, - "workingDirectories": { - "type": "array", - "items": { - "$ref": "#/$defs/URI" - }, - "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}." + "import": { + "$ref": "#/$defs/AutomationImport", + "description": "Optional legacy import state. When present, {@link definition} MUST be\ndisabled so automatic triggers cannot run before migration cutover." } }, "required": [ "channel", - "chat" + "definition" ] }, - "DisposeChatParams": { + "AutomationDefinitionPatch": { "type": "object", - "description": "Disposes a chat and cleans up server-side resources.", + "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": { - "channel": { - "$ref": "#/$defs/URI", - "description": "Channel URI this command targets." + "title": { + "type": "string", + "description": "Replacement human-readable title." + }, + "message": { + "$ref": "#/$defs/Message", + "description": "Replacement initial user message." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Replacement session template." + }, + "enabled": { + "type": "boolean", + "description": "Replacement automatic-trigger enabled state." + }, + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + }, + "description": "Complete replacement trigger list." }, "_meta": { "type": "object", "additionalProperties": {}, - "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + "description": "Complete replacement implementation-defined metadata." } - }, - "required": [ - "channel" - ] + } }, - "CreateTerminalParams": { + "UpdateAutomationParams": { "type": "object", - "description": "Creates a new terminal on the server.\n\nAfter creation, the client should subscribe to the terminal URI to receive\nstate updates. The server dispatches `root/terminalsChanged` to update the\nroot terminal list.", + "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", - "description": "Terminal URI (client-chosen)." + "description": "Target `ahp-automation:` URI." }, "_meta": { "type": "object", "additionalProperties": {}, "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." }, - "claim": { - "$ref": "#/$defs/TerminalClaim", - "description": "Initial owner of the terminal" - }, - "name": { - "type": "string", - "description": "Human-readable terminal name" - }, - "cwd": { - "$ref": "#/$defs/URI", - "description": "Initial working directory URI" - }, - "cols": { + "expectedRevision": { "type": "number", - "description": "Initial terminal width in columns" + "description": "Revision on which the client based {@link changes}." }, - "rows": { - "type": "number", - "description": "Initial terminal height in rows" + "changes": { + "$ref": "#/$defs/AutomationDefinitionPatch", + "description": "Editable fields to replace." } }, "required": [ "channel", - "claim" + "expectedRevision", + "changes" ] }, - "DisposeTerminalParams": { + "DisposeAutomationParams": { "type": "object", - "description": "Disposes a terminal and kills its process if still running.\n\nThe server dispatches `root/terminalsChanged` to remove the terminal from\nthe root terminal list.", + "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", @@ -6358,133 +7581,113 @@ "channel" ] }, - "ChangesetOperationFollowUp": { - "type": "object", - "description": "Optional follow-up surfaced by the server after an operation completes —\na {@link ContentRef} the client can fetch and display.\n\nSet `external` to `true` to open the content in the user's preferred\nexternal handler (e.g. browser); otherwise the client is expected to\nsurface it inline.", - "properties": { - "content": { - "$ref": "#/$defs/ContentRef" - }, - "external": { - "type": "boolean", - "description": "When `true`, open in an external handler rather than inline." - } - }, - "required": [ - "content" - ] - }, - "InvokeChangesetOperationParams": { + "RunAutomationParams": { "type": "object", - "description": "Invokes a server-defined {@link ChangesetOperation} against a changeset,\na single file, or a line range.\n\nThe server validates that `operationId` exists in the changeset's\ncurrent `operations` list and that the requested `target.kind` is\ncontained in the operation's `scopes`. Invalid combinations result in a\nJSON-RPC error.\n\nState changes resulting from invocation flow back through the normal\n`changeset/*` action stream on the relevant changeset URIs. Clients\nSHOULD NOT synthesise local optimistic changes for invocations unless\nthe server explicitly opts in via a future capability.", + "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", - "description": "The expanded changeset 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." }, - "operationId": { + "requestId": { "type": "string", - "description": "Matches {@link ChangesetOperation.id} from the changeset's `operations` list." - }, - "target": { - "$ref": "#/$defs/ChangesetOperationTarget", - "description": "Target of the operation. Required iff the chosen scope is\n`'resource'` or `'range'`. Omit for changeset-scoped operations." + "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": [ "channel", - "operationId" + "requestId" ] }, - "InvokeChangesetOperationResult": { + "RunAutomationResult": { "type": "object", - "description": "Result of the {@link InvokeChangesetOperationParams | `invokeChangesetOperation`}\ncommand.\n\nSuccess is implicit: the server returns this result when it accepted\nthe operation. Failure is signalled by rejecting the JSON-RPC request\nwith an appropriate error code, not by any field on this result. The\noperation MAY still produce subsequent failure feedback through the\n{@link ChangesetStatusChangedAction | `changeset/statusChanged`} stream.", + "description": "Result identifying the existing or newly created run.", "properties": { - "message": { - "$ref": "#/$defs/StringOrMarkdown", - "description": "Optional human-readable message describing the result." - }, - "followUp": { - "$ref": "#/$defs/ChangesetOperationFollowUp", - "description": "Optional follow-up: a URI to open (e.g. a PR), a content ref, etc." + "run": { + "$ref": "#/$defs/URI", + "description": "Subscribable `ahp-automation-run:` URI." } - } + }, + "required": [ + "run" + ] }, - "CreateResourceWatchParams": { + "FetchAutomationRunsParams": { "type": "object", - "description": "Creates a resource watcher on the receiver's filesystem.\n\nThe receiver allocates an `ahp-resource-watch:/` channel URI and\nreturns it on {@link CreateResourceWatchResult.channel}. The caller then\n[`subscribe`](./subscriptions)s to that channel to receive\n`resourceWatch/changed` actions over the standard action envelope.\n\nThe watch lifecycle is tied to subscription: when every subscriber has\nunsubscribed (or the underlying connection drops), the receiver MUST\nrelease the watcher. There is no explicit dispose command — `unsubscribe`\nis the only handle the caller needs.\n\nLike the rest of the `resource*` family, `createResourceWatch` is\nsymmetrical and MAY be sent in either direction. Access is gated through\nthe same permission flow as `resourceRead`/`resourceWrite`.", + "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": { - "type": "string", - "enum": [ - "ahp-root://" - ] + "$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." }, - "uri": { - "$ref": "#/$defs/URI", - "description": "URI to watch." - }, - "recursive": { - "type": "boolean", - "description": "If `true`, the receiver MUST report changes for descendants of `uri`.\nIf `false` (default), only changes to `uri` itself — and, when `uri`\nis a directory, its direct children — are reported." - }, - "excludes": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "items" + "cursor": { + "type": "string", + "description": "Cursor previously received as {@link AutomationState.runsNextCursor}.\nOmit to request the first page not already included by the snapshot." + } + }, + "required": [ + "channel" + ] + }, + "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": "Glob patterns or paths relative to `uri` to exclude from reporting.\nWrapped in `{ items }` for forward compatibility." + "description": "Schedule preview is requested from the root channel." }, - "includes": { + "_meta": { "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." + "additionalProperties": {}, + "description": "Optional JSON-serializable metadata associated with this request.\nReceivers MUST ignore keys they do not understand." + }, + "schedule": { + "$ref": "#/$defs/AutomationSchedule", + "description": "Portable AHP cron schedule to evaluate." + }, + "count": { + "type": "number", + "description": "Requested maximum number of future occurrences; the host MAY cap it." } }, "required": [ "channel", - "uri" + "schedule" ] }, - "CreateResourceWatchResult": { + "PreviewAutomationScheduleResult": { "type": "object", - "description": "Result of the `createResourceWatch` command.", + "description": "Host-canonical future schedule occurrences.", "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" + }, + "description": "Ascending ISO 8601 timestamps." } }, "required": [ - "channel" + "items" ] }, "URI": { @@ -6543,6 +7746,10 @@ "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", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." + }, "SessionLifecycle": { "enum": [ "creating", @@ -7122,6 +8329,93 @@ "type": "string", "description": "Discriminant for {@link ResourceChange.type}." }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." + }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$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": [ + "update", + "dispose", + "run" + ], + "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": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "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": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ], + "description": "Immutable provenance describing why a run was created." + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ], + "description": "Discriminated lifecycle of an automation run." + }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string", + "description": "Operations the host currently permits for a run." + }, + "AutomationExecutionLifetime": { + "enum": [ + "hostLifetime", + "managed" + ], + "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", "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.", @@ -7405,6 +8699,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." @@ -9453,6 +10780,216 @@ "changes" ] }, + "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", + "description": "Complete replacement definition." + }, + "revision": { + "type": "number", + "description": "New monotonic revision." + }, + "modifiedAt": { + "type": "string", + "description": "Definition modification timestamp in ISO 8601 format." + }, + "nextRunAt": { + "type": "string", + "description": "Earliest known future scheduled occurrence, or omitted to clear it." + } + }, + "required": [ + "type", + "definition", + "revision", + "modifiedAt" + ] + }, + "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", + "description": "New or replacement run summary." + } + }, + "required": [ + "type", + "run" + ] + }, + "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", + "description": "{@link AutomationRunSummary.resource} to remove." + } + }, + "required": [ + "type", + "run" + ] + }, + "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" + }, + "runs": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Older run summaries in newest-first order within this page." + }, + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next older page, or omitted at the end." + } + }, + "required": [ + "type", + "runs" + ] + }, + "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", + "description": "Complete replacement lifecycle." + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationRunOperation" + }, + "description": "Complete replacement operation list." + } + }, + "required": [ + "type", + "lifecycle", + "operations" + ] + }, + "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", + "description": "Session URI to append when it is not already linked." + } + }, + "required": [ + "type", + "session" + ] + }, + "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", + "description": "Linked session URI to remove." + } + }, + "required": [ + "type", + "session" + ] + }, + "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", + "description": "New primary linked session, or omitted to clear the selection." + } + }, + "required": [ + "type" + ] + }, + "AutomationRunArtifactSetAction": { + "type": "object", + "description": "Upsert a run-scoped artifact by {@link AutomationRunArtifact.id}.", + "properties": { + "type": { + "const": "automationRun/artifactSet" + }, + "artifact": { + "$ref": "#/$defs/AutomationRunArtifact", + "description": "New or replacement artifact." + } + }, + "required": [ + "type", + "artifact" + ] + }, + "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", + "description": "{@link AutomationRunArtifact.id} to remove." + } + }, + "required": [ + "type", + "artifactId" + ] + }, + "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" + } + }, + "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 19ab77673..9122ebb99 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,60 @@ "changes" ] }, + "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", + "description": "Root channel URI." + }, + "summary": { + "$ref": "#/$defs/AutomationSummary", + "description": "Complete summary for the newly visible automation." + } + }, + "required": [ + "channel", + "summary" + ] + }, + "AutomationRemovedParams": { + "type": "object", + "description": "Announces that an automation is no longer present in the root catalogue.", + "properties": { + "channel": { + "$ref": "#/$defs/URI", + "description": "Root channel URI." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Removed `ahp-automation:` URI." + } + }, + "required": [ + "channel", + "automation" + ] + }, + "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", + "description": "Root channel URI." + }, + "summary": { + "$ref": "#/$defs/AutomationSummary", + "description": "Complete replacement catalogue summary." + } + }, + "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 +301,15 @@ { "$ref": "#/$defs/SessionSummaryChangedParams" }, + { + "$ref": "#/$defs/AutomationAddedParams" + }, + { + "$ref": "#/$defs/AutomationRemovedParams" + }, + { + "$ref": "#/$defs/AutomationSummaryChangedParams" + }, { "$ref": "#/$defs/ProgressParams" }, @@ -670,6 +737,12 @@ }, { "$ref": "#/$defs/ChatState" + }, + { + "$ref": "#/$defs/AutomationState" + }, + { + "$ref": "#/$defs/AutomationRunState" } ], "description": "The current state of the resource" @@ -897,6 +970,28 @@ "values" ] }, + "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", + "description": "Owning `ahp-automation:` URI." + }, + "run": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` 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 +1012,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 +1058,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 +1366,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" @@ -5157,140 +5264,862 @@ "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" - }, - { - "type": "number" - }, - { - "type": "boolean" + "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": { + "expression": { + "type": "string", + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, - { - "type": "null" + "timeZone": { + "type": "string", + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } - ], - "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "required": [ + "expression", + "timeZone" + ] }, - "Customization": { - "oneOf": [ - { - "$ref": "#/$defs/PluginCustomization" + "AutomationScheduleTrigger": { + "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", + "properties": { + "id": { + "type": "string", + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - { - "$ref": "#/$defs/DirectoryCustomization" + "kind": { + "const": "schedule" }, - { - "$ref": "#/$defs/McpServerCustomization" + "schedule": { + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." + }, + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } - ], - "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": [ + "id", + "kind", + "schedule" + ] }, - "SessionInputRequest": { - "oneOf": [ - { - "$ref": "#/$defs/SessionChatInputRequest" + "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": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - { - "$ref": "#/$defs/SessionToolConfirmationRequest" + "kind": { + "const": "event" }, - { - "$ref": "#/$defs/SessionToolClientExecutionRequest" + "type": { + "type": "string", + "description": "Matches {@link AutomationTriggerDefinition.type}." }, - { - "$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" + "events": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, - { - "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } - ], - "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": [ + "id", + "kind", + "type", + "events" + ] }, - "ToolCallState": { - "oneOf": [ - { - "$ref": "#/$defs/ToolCallStreamingState" + "AutomationTriggerEventDefinition": { + "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", + "properties": { + "id": { + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, - { - "$ref": "#/$defs/ToolCallPendingConfirmationState" + "title": { + "type": "string", + "description": "Human-readable label suitable for selection UI." }, - { - "$ref": "#/$defs/ToolCallRunningState" + "description": { + "type": "string", + "description": "Optional longer explanation of when this event fires." + } + }, + "required": [ + "id", + "title" + ] + }, + "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", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, - { - "$ref": "#/$defs/ToolCallAuthRequiredState" + "title": { + "type": "string", + "description": "Human-readable trigger type name." }, - { - "$ref": "#/$defs/ToolCallPendingResultConfirmationState" + "description": { + "type": "string", + "description": "Optional longer explanation of the trigger source." }, - { - "$ref": "#/$defs/ToolCallCompletedState" + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTriggerEventDefinition" + }, + "description": "Events clients may select for this trigger type." }, - { - "$ref": "#/$defs/ToolCallCancelledState" + "configSchema": { + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } - ], - "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": [ + "type", + "title", + "events" + ] }, - "CustomizationLoadState": { - "oneOf": [ - { - "$ref": "#/$defs/CustomizationLoadingState" - }, - { - "$ref": "#/$defs/CustomizationLoadedState" + "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", + "description": "Provider id. Omit to use the host's default provider." + }, + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." + }, + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." + }, + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "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", + "description": "Human-readable automation name." + }, + "message": { + "$ref": "#/$defs/Message", + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." + }, + "session": { + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." + }, + "enabled": { + "type": "boolean", + "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": "Automatic triggers. An empty list means manual-only." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." + } + }, + "required": [ + "title", + "message", + "session", + "enabled", + "triggers" + ] + }, + "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": {}, + "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", + "description": "Subscribable `ahp-automation:` URI." + }, + "title": { + "type": "string", + "description": "Current {@link AutomationDefinition.title}." + }, + "enabled": { + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." + }, + "triggerCount": { + "type": "number", + "description": "Number of automatic triggers in the current definition." + }, + "nextRunAt": { + "type": "string", + "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", + "description": "Most recent retained run, when any run exists." + }, + "revision": { + "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", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "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", + "description": "URI of this automation channel." + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." + }, + "revision": { + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." + }, + "nextRunAt": { + "type": "string", + "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", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." + }, + "runsNextCursor": { + "type": "string", + "description": "Opaque cursor for the next older run-history page." + }, + "runtime": { + "$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", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." + } + }, + "required": [ + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationRunBlocker": { + "type": "object", + "description": "Summary of why a run cannot currently make progress.", + "properties": { + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "description": "Cause recorded for a client-requested manual run.", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", + "properties": { + "kind": { + "const": "trigger" + }, + "triggerId": { + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." + }, + "scheduledFor": { + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." + }, + "catchUp": { + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "description": "A durable run exists but has not begun external execution.", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "description": "The run is actively executing linked sessions.", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "description": "The run started but is temporarily unable to progress.", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "completedAt": { + "type": "string", + "description": "Completion timestamp in ISO 8601 format." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Failure timestamp in ISO 8601 format." + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "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", + "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", + "description": "Stable artifact id within this run, used by artifact actions." + }, + "label": { + "type": "string", + "description": "Human-readable label suitable for run-history UI." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "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", + "description": "Subscribable `ahp-automation-run:` URI." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." + }, + "primarySession": { + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." + }, + "sessionCount": { + "type": "number", + "description": "Number of linked sessions, including attempts and workers." + }, + "artifactCount": { + "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": {}, + "description": "Opaque host-defined summary metadata." + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "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", + "description": "URI of this automation-run channel." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$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", + "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": {}, + "description": "Opaque host-defined run metadata." + } + }, + "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", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." + }, + "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" @@ -5799,6 +6628,85 @@ ], "type": "string", "description": "Discriminant for {@link ResourceChange.type}." + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." + }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$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": [ + "update", + "dispose", + "run" + ], + "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": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "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": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ], + "description": "Immutable provenance describing why a run was created." + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ], + "description": "Discriminated lifecycle of an automation run." + }, + "AutomationRunOperation": { + "enum": [ + "cancel" + ], + "type": "string", + "description": "Operations the host currently permits for a run." } } } diff --git a/schema/state.schema.json b/schema/state.schema.json index be1850022..3bbe1caae 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,28 @@ "values" ] }, + "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", + "description": "Owning `ahp-automation:` URI." + }, + "run": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation-run:` 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 +693,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 +739,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 +1047,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" @@ -4905,173 +4945,895 @@ "type" ] }, - "StringOrMarkdown": { - "oneOf": [ - { - "type": "string" + "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": { + "expression": { + "type": "string", + "description": "Five-field AHP cron expression described by {@link AutomationSchedule}." }, - { - "type": "object", - "properties": { - "markdown": { - "type": "string" - } - }, - "required": [ - "markdown" - ] + "timeZone": { + "type": "string", + "description": "IANA Time Zone Database identifier used to interpret the expression, for\nexample `\"UTC\"` or `\"Europe/Berlin\"`." } - ], - "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": [ + "expression", + "timeZone" + ] }, - "JsonPrimitive": { - "oneOf": [ - { - "type": "string" + "AutomationScheduleTrigger": { + "type": "object", + "description": "Starts runs from a recurring cron schedule evaluated by the host.", + "properties": { + "id": { + "type": "string", + "description": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - { - "type": "number" + "kind": { + "const": "schedule" }, - { - "type": "boolean" + "schedule": { + "$ref": "#/$defs/AutomationSchedule", + "description": "Recurrence and time zone evaluated by the host." }, - { - "type": "null" + "misfirePolicy": { + "$ref": "#/$defs/AutomationMisfirePolicy", + "description": "Policy for missed occurrences. Omission is equivalent to\n{@link AutomationMisfirePolicy.RunOnce}." } - ], - "description": "A primitive JSON value: a string, number, boolean, or `null`." + }, + "required": [ + "id", + "kind", + "schedule" + ] }, - "SessionInputRequest": { - "oneOf": [ - { - "$ref": "#/$defs/SessionChatInputRequest" - }, - { - "$ref": "#/$defs/SessionToolConfirmationRequest" + "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": "Identifier unique and stable within this automation definition. Run causes\nrefer back to this value." }, - { - "$ref": "#/$defs/SessionToolClientExecutionRequest" + "kind": { + "const": "event" }, - { - "$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." - }, - "CustomizationEnablement": { - "oneOf": [ - { - "type": "object", - "properties": { - "kind": { - "const": "global" - }, - "enabled": { - "type": "boolean" - } - }, - "required": [ - "kind", - "enabled" - ] + "type": { + "type": "string", + "description": "Matches {@link AutomationTriggerDefinition.type}." }, - { - "type": "object", - "properties": { - "kind": { - "const": "workspace" - }, - "uri": { - "$ref": "#/$defs/URI" - }, - "enabled": { - "type": "boolean" - } + "events": { + "type": "array", + "items": { + "type": "string" }, - "required": [ - "kind", - "uri", - "enabled" - ] + "description": "Selected {@link AutomationTriggerEventDefinition.id | event ids} for this\ntrigger type." }, - { + "config": { "type": "object", - "properties": { - "kind": { - "const": "session" - }, - "enabled": { - "type": "boolean" - } - }, - "required": [ - "kind", - "enabled" - ] + "additionalProperties": {}, + "description": "Values described by {@link AutomationTriggerDefinition.configSchema}.\nClients MUST preserve unknown entries when editing other fields." } - ], - "description": "A single explicit enablement decision." + }, + "required": [ + "id", + "kind", + "type", + "events" + ] }, - "ChildCustomizationType": { - "oneOf": [ - { - "const": "agent" + "AutomationTriggerEventDefinition": { + "type": "object", + "description": "One selectable event exposed by a host-defined trigger type.", + "properties": { + "id": { + "type": "string", + "description": "Stable event id stored in {@link AutomationEventTrigger.events}." }, - { - "const": "skill" + "title": { + "type": "string", + "description": "Human-readable label suitable for selection UI." }, - { - "const": "prompt" + "description": { + "type": "string", + "description": "Optional longer explanation of when this event fires." + } + }, + "required": [ + "id", + "title" + ] + }, + "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", + "description": "Stable type id stored in {@link AutomationEventTrigger.type}." }, - { - "const": "rule" + "title": { + "type": "string", + "description": "Human-readable trigger type name." }, - { - "const": "hook" + "description": { + "type": "string", + "description": "Optional longer explanation of the trigger source." }, - { - "const": "mcpServer" + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTriggerEventDefinition" + }, + "description": "Events clients may select for this trigger type." + }, + "configSchema": { + "$ref": "#/$defs/ConfigSchema", + "description": "Optional schema for {@link AutomationEventTrigger.config}." } - ], - "description": "Customization types that appear as children of a\n{@link PluginCustomization} or {@link DirectoryCustomization}." + }, + "required": [ + "type", + "title", + "events" + ] }, - "CustomizationLoadState": { - "oneOf": [ - { - "$ref": "#/$defs/CustomizationLoadingState" + "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", + "description": "Provider id. Omit to use the host's default provider." }, - { - "$ref": "#/$defs/CustomizationLoadedState" + "model": { + "$ref": "#/$defs/ModelSelection", + "description": "Optional model selection resolved when a run starts." }, - { - "$ref": "#/$defs/CustomizationDegradedState" + "agent": { + "$ref": "#/$defs/AgentSelection", + "description": "Optional custom agent selection resolved when a run starts." }, - { - "$ref": "#/$defs/CustomizationErrorState" + "workingDirectories": { + "type": "array", + "items": { + "$ref": "#/$defs/URI" + }, + "description": "Ordered working-directory URIs for each created session. Absence means a\nworkspace-less session." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Session configuration values accepted by `createSession`, normally\nobtained from `resolveSessionConfig`." } - ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + } }, - "ChildCustomization": { - "oneOf": [ - { - "$ref": "#/$defs/AgentCustomization" + "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", + "description": "Human-readable automation name." }, - { - "$ref": "#/$defs/SkillCustomization" + "message": { + "$ref": "#/$defs/Message", + "description": "Initial message sent to every newly created run session. Its origin MUST be\n`user`." }, - { - "$ref": "#/$defs/PromptCustomization" + "session": { + "$ref": "#/$defs/AutomationSessionTemplate", + "description": "Template used to create fresh sessions for each run." }, - { - "$ref": "#/$defs/RuleCustomization" + "enabled": { + "type": "boolean", + "description": "Whether automatic triggers may create runs. Manual runs remain available\nwhenever {@link AutomationOperation.Run} is advertised." }, - { - "$ref": "#/$defs/HookCustomization" + "triggers": { + "type": "array", + "items": { + "$ref": "#/$defs/AutomationTrigger" + }, + "description": "Automatic triggers. An empty list means manual-only." }, - { - "$ref": "#/$defs/McpServerCustomization" - } + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque implementation-defined metadata. Clients MUST preserve unknown\nentries when updating the definition." + } + }, + "required": [ + "title", + "message", + "session", + "enabled", + "triggers" + ] + }, + "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": {}, + "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", + "description": "Subscribable `ahp-automation:` URI." + }, + "title": { + "type": "string", + "description": "Current {@link AutomationDefinition.title}." + }, + "enabled": { + "type": "boolean", + "description": "Current {@link AutomationDefinition.enabled} value." + }, + "triggerCount": { + "type": "number", + "description": "Number of automatic triggers in the current definition." + }, + "nextRunAt": { + "type": "string", + "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", + "description": "Most recent retained run, when any run exists." + }, + "revision": { + "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", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined catalogue metadata." + } + }, + "required": [ + "resource", + "title", + "enabled", + "triggerCount", + "revision", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "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", + "description": "URI of this automation channel." + }, + "definition": { + "$ref": "#/$defs/AutomationDefinition", + "description": "Current durable definition." + }, + "revision": { + "type": "number", + "description": "Monotonically increasing definition revision. Clients pass the revision\nthey observed as `updateAutomation.expectedRevision`." + }, + "nextRunAt": { + "type": "string", + "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", + "items": { + "$ref": "#/$defs/AutomationRunSummary" + }, + "description": "Newest-first retained run summaries. This is a bounded window; use\n`fetchAutomationRuns` when {@link runsNextCursor} is present." + }, + "runsNextCursor": { + "type": "string", + "description": "Opaque cursor for the next older run-history page." + }, + "runtime": { + "$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", + "description": "Creation timestamp in ISO 8601 format." + }, + "modifiedAt": { + "type": "string", + "description": "Last definition modification timestamp in ISO 8601 format." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined state metadata." + } + }, + "required": [ + "resource", + "definition", + "revision", + "runs", + "operations", + "createdAt", + "modifiedAt" + ] + }, + "AutomationRunBlocker": { + "type": "object", + "description": "Summary of why a run cannot currently make progress.", + "properties": { + "kind": { + "$ref": "#/$defs/AutomationRunBlockerKind", + "description": "Category of the outstanding dependency." + } + }, + "required": [ + "kind" + ] + }, + "AutomationManualRunCause": { + "type": "object", + "description": "Cause recorded for a client-requested manual run.", + "properties": { + "kind": { + "const": "manual" + } + }, + "required": [ + "kind" + ] + }, + "AutomationTriggeredRunCause": { + "type": "object", + "description": "Cause recorded for a run created by one of the automation's triggers.", + "properties": { + "kind": { + "const": "trigger" + }, + "triggerId": { + "type": "string", + "description": "Matches the stable {@link AutomationTrigger.id} in the definition." + }, + "scheduledFor": { + "type": "string", + "description": "Intended schedule occurrence as an ISO 8601 timestamp. Present for\nschedule triggers and normally absent for event triggers." + }, + "catchUp": { + "type": "boolean", + "description": "`true` when this is a catch-up run created by\n{@link AutomationMisfirePolicy.RunOnce}." + }, + "event": { + "type": "object", + "additionalProperties": {}, + "description": "Host-defined, non-secret event provenance suitable for display or audit.\nThis is descriptive context, not an input that clients replay." + } + }, + "required": [ + "kind", + "triggerId" + ] + }, + "AutomationPendingRunLifecycle": { + "type": "object", + "description": "A durable run exists but has not begun external execution.", + "properties": { + "status": { + "const": "pending" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt" + ] + }, + "AutomationRunningRunLifecycle": { + "type": "object", + "description": "The run is actively executing linked sessions.", + "properties": { + "status": { + "const": "running" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "startedAt" + ] + }, + "AutomationBlockedRunLifecycle": { + "type": "object", + "description": "The run started but is temporarily unable to progress.", + "properties": { + "status": { + "const": "blocked" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "blocker": { + "$ref": "#/$defs/AutomationRunBlocker", + "description": "Coarse blocker summary; linked sessions contain interaction details." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "blocker" + ] + }, + "AutomationCompletedRunLifecycle": { + "type": "object", + "description": "Terminal lifecycle for a successfully completed run.", + "properties": { + "status": { + "const": "completed" + }, + "createdAt": { + "type": "string", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format." + }, + "completedAt": { + "type": "string", + "description": "Completion timestamp in ISO 8601 format." + }, + "usage": { + "$ref": "#/$defs/UsageInfo", + "description": "Optional aggregate model usage across all linked sessions." + } + }, + "required": [ + "status", + "createdAt", + "startedAt", + "completedAt" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Failure timestamp in ISO 8601 format." + }, + "error": { + "$ref": "#/$defs/ErrorInfo", + "description": "Stable machine-readable and human-readable failure information." + } + }, + "required": [ + "status", + "createdAt", + "completedAt", + "error" + ] + }, + "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", + "description": "Run creation timestamp in ISO 8601 format." + }, + "startedAt": { + "type": "string", + "description": "First execution start timestamp in ISO 8601 format, when execution began." + }, + "completedAt": { + "type": "string", + "description": "Cancellation completion timestamp in ISO 8601 format." + } + }, + "required": [ + "status", + "createdAt", + "completedAt" + ] + }, + "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", + "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", + "description": "Stable artifact id within this run, used by artifact actions." + }, + "label": { + "type": "string", + "description": "Human-readable label suitable for run-history UI." + }, + "_meta": { + "type": "object", + "additionalProperties": {}, + "description": "Opaque host-defined artifact metadata." + } + }, + "required": [ + "uri", + "id", + "label" + ] + }, + "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", + "description": "Subscribable `ahp-automation-run:` URI." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$ref": "#/$defs/AutomationRunLifecycle", + "description": "Current or terminal lifecycle snapshot." + }, + "primarySession": { + "$ref": "#/$defs/URI", + "description": "Session the host recommends opening first, when one has been selected." + }, + "sessionCount": { + "type": "number", + "description": "Number of linked sessions, including attempts and workers." + }, + "artifactCount": { + "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": {}, + "description": "Opaque host-defined summary metadata." + } + }, + "required": [ + "resource", + "automation", + "cause", + "lifecycle", + "sessionCount", + "operations" + ] + }, + "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", + "description": "URI of this automation-run channel." + }, + "automation": { + "$ref": "#/$defs/URI", + "description": "Owning `ahp-automation:` URI." + }, + "cause": { + "$ref": "#/$defs/AutomationRunCause", + "description": "Immutable reason this run was created." + }, + "lifecycle": { + "$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", + "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": {}, + "description": "Opaque host-defined run metadata." + } + }, + "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", + "description": "Durable provenance for sessions created by a higher-level AHP workflow." + }, + "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." + }, + "CustomizationEnablement": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "global" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "uri": { + "$ref": "#/$defs/URI" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "uri", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "session" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + } + ], + "description": "A single explicit enablement decision." + }, + "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}." }, @@ -5390,6 +6152,51 @@ ], "description": "A content part within terminal output." }, + "AutomationTrigger": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationScheduleTrigger" + }, + { + "$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": [ + { + "$ref": "#/$defs/AutomationManualRunCause" + }, + { + "$ref": "#/$defs/AutomationTriggeredRunCause" + } + ], + "description": "Immutable provenance describing why a run was created." + }, + "AutomationRunLifecycle": { + "oneOf": [ + { + "$ref": "#/$defs/AutomationPendingRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationRunningRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationBlockedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCompletedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationFailedRunLifecycle" + }, + { + "$ref": "#/$defs/AutomationCancelledRunLifecycle" + } + ], + "description": "Discriminated lifecycle of an automation run." + }, "URI": { "type": "string", "description": "A URI string (e.g. `ahp-root://`, `ahp-session:/`, or `ahp-chat:/`)." @@ -5539,6 +6346,40 @@ ], "type": "string", "description": "Discriminant for {@link ResourceChange.type}." + }, + "AutomationMisfirePolicy": { + "enum": [ + "skip", + "runOnce" + ], + "type": "string", + "description": "How a host handles schedule occurrences missed while automatic execution was\nunavailable." + }, + "AutomationOperation": { + "enum": [ + "update", + "dispose", + "run" + ], + "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": [ + "userInput", + "toolConfirmation", + "authentication", + "clientExecution" + ], + "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", + "description": "Operations the host currently permits for a run." } } } 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 63b508b23..4abdbfd9e 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,10 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [ @@ -829,6 +849,29 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, + { name: 'AutomationSessionOrigin' }, + { name: 'AutomationSchedule' }, + { 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 +1097,53 @@ 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_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 +1244,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 +1258,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 +1297,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 { @@ -1358,6 +1468,14 @@ 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_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()); @@ -1458,6 +1576,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 { @@ -1563,7 +1692,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: 'AutomationRunCancellationCapability' }, { name: 'AutomationSchedulePreviewCapability' }, + { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, @@ -1593,6 +1726,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: 'AutomationImport' }, { name: 'AutomationImportTriggerNextRun' }, { name: 'AutomationDefinitionPatch' }, + { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, + { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, + { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, + { name: 'PreviewAutomationScheduleParams' }, { name: 'PreviewAutomationScheduleResult' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1804,6 +1944,9 @@ const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'AutomationAddedParams', + 'AutomationRemovedParams', + 'AutomationSummaryChangedParams', 'ProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', @@ -2159,6 +2302,10 @@ function checkExhaustiveness(project: Project): void { 'SessionInputRequest', 'ToolCallConfirmationState', 'ReconnectResult', + 'SessionOrigin', + 'AutomationTrigger', + 'AutomationRunCause', + 'AutomationRunLifecycle', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index 7912b6d3e..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('inherits request metadata from BaseParams', () => { + it('preserves automation schedule restrictions', () => { 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/); + const schedules = defs.AutomationScheduleCapabilities; + const properties = schedules.properties as Record>; + 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 2c2f17cb0..44a9d62a3 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,10 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; const STATE_STRUCTS = [ @@ -958,6 +989,16 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', + 'AutomationSessionOrigin', 'AutomationSchedule', + '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 +1262,49 @@ 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_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]; @@ -1302,6 +1386,14 @@ 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_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()); @@ -1398,6 +1490,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. */ @@ -1558,7 +1661,11 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', - 'ClientCapabilities', 'Implementation', + 'ClientCapabilities', 'AutomationCapabilities', + 'AutomationExecutionCapabilities', 'AutomationCreateCapability', + 'AutomationScheduleCapabilities', + 'AutomationRunCancellationCapability', 'AutomationSchedulePreviewCapability', + 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', @@ -1585,6 +1692,12 @@ const COMMAND_STRUCTS = [ 'CompletionsParams', 'CompletionItem', 'CompletionsResult', 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', + 'ListAutomationsParams', 'ListAutomationsResult', + 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', + 'CreateAutomationParams', 'AutomationImport', 'AutomationImportTriggerNextRun', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', + 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', + 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1817,6 +1930,9 @@ const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'AutomationAddedParams', + 'AutomationRemovedParams', + 'AutomationSummaryChangedParams', 'ProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', @@ -2192,6 +2308,10 @@ 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 + '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 9efbe80d3..6af678cfe 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,10 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; /** @@ -812,6 +817,29 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'TelemetryCapabilities' }, { name: 'ResourceWatchState' }, { name: 'ResourceChange' }, + { name: 'AutomationSessionOrigin', omitDiscriminants: true }, + { name: 'AutomationSchedule' }, + { 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 +1070,49 @@ 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_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 +1159,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 +1175,8 @@ pub enum SnapshotState { Changeset(Box), ResourceWatch(Box), Annotations(Box), + Automation(Box), + AutomationRun(Box), Root(Box), }`; } @@ -1195,6 +1267,14 @@ 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_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(''); @@ -1296,6 +1376,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 { @@ -1334,7 +1425,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, CustomizationEnablement, 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, CustomizationEnablement, 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 @@ -1437,7 +1528,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: 'AutomationRunCancellationCapability' }, { name: 'AutomationSchedulePreviewCapability' }, + { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, @@ -1468,6 +1563,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: 'AutomationImport' }, { name: 'AutomationImportTriggerNextRun' }, { name: 'AutomationDefinitionPatch' }, + { name: 'UpdateAutomationParams' }, { name: 'DisposeAutomationParams' }, + { name: 'RunAutomationParams' }, { name: 'RunAutomationResult' }, + { name: 'FetchAutomationRunsParams' }, { name: 'FetchAutomationRunsResult' }, + { name: 'PreviewAutomationScheduleParams' }, { name: 'PreviewAutomationScheduleResult' }, ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1495,7 +1597,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, AutomationSessionTemplate, AutomationSummary, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); @@ -1626,6 +1728,9 @@ const NOTIFICATION_STRUCTS = [ 'SessionAddedParams', 'SessionRemovedParams', 'SessionSummaryChangedParams', + 'AutomationAddedParams', + 'AutomationRemovedParams', + 'AutomationSummaryChangedParams', 'ProgressParams', 'AuthRequiredParams', 'OtlpExportLogsParams', @@ -1636,7 +1741,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, ProtectedResourceMetadata, SessionStatus, SessionSummary};'); + lines.push('use crate::state::{AgentSelection, AnnotationsSummary, AutomationOperation, AutomationRunSummary, AutomationSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, ProtectedResourceMetadata, SessionOrigin, SessionStatus, SessionSummary};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); @@ -1943,6 +2048,10 @@ function checkExhaustiveness(project: Project): void { 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ReconnectResult', + 'SessionOrigin', + 'AutomationTrigger', + 'AutomationRunCause', + 'AutomationRunLifecycle', 'AuthRequiredErrorData', 'PermissionDeniedErrorData', 'UnsupportedProtocolVersionErrorData', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 1893f4e5d..1fc0d4d02 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,10 @@ const STATE_ENUMS = [ 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', + 'SessionOriginKind', + 'AutomationOperation', 'AutomationExecutionLifetime', 'AutomationMisfirePolicy', 'AutomationTriggerKind', + 'AutomationRunStatus', 'AutomationRunBlockerKind', 'AutomationRunCauseKind', + 'AutomationRunOperation', ]; const STATE_STRUCTS = [ @@ -667,6 +680,16 @@ const STATE_STRUCTS = [ 'AnnotationsSummary', 'AnnotationsState', 'Annotation', 'AnnotationEntry', 'TelemetryCapabilities', 'ResourceWatchState', 'ResourceChange', + 'AutomationSessionOrigin', 'AutomationSchedule', + '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 +1018,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 +1027,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 +1047,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 +1065,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 +1152,49 @@ 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_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]; @@ -1194,6 +1268,14 @@ 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_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()); @@ -1291,6 +1373,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 */ @@ -1460,7 +1553,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', + 'AutomationRunCancellationCapability', 'AutomationSchedulePreviewCapability', + 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', @@ -1487,6 +1584,12 @@ const COMMAND_STRUCTS = [ 'CompletionsParams', 'CompletionItem', 'CompletionsResult', 'InvokeChangesetOperationParams', 'InvokeChangesetOperationResult', 'ChangesetOperationFollowUp', + 'ListAutomationsParams', 'ListAutomationsResult', + 'ListAutomationTriggerDefinitionsParams', 'ListAutomationTriggerDefinitionsResult', + 'CreateAutomationParams', 'AutomationImport', 'AutomationImportTriggerNextRun', 'AutomationDefinitionPatch', 'UpdateAutomationParams', + 'DisposeAutomationParams', 'RunAutomationParams', 'RunAutomationResult', + 'FetchAutomationRunsParams', 'FetchAutomationRunsResult', + 'PreviewAutomationScheduleParams', 'PreviewAutomationScheduleResult', ]; const RECONNECT_RESULT_UNION: UnionConfig = { @@ -1716,7 +1819,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', ]; @@ -2204,6 +2309,10 @@ 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 + '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..6d668f0e3 --- /dev/null +++ b/types/channels-automation-run/actions.ts @@ -0,0 +1,112 @@ +/** + * Automation Run Channel Actions — mutations and side-effect requests scoped + * to an `ahp-automation-run:` channel. + * + * @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'; + +/** + * 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[]; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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 { + 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..43d14488b --- /dev/null +++ b/types/channels-automation-run/state.ts @@ -0,0 +1,314 @@ +/** + * Automation Run Channel State Types. + * + * @module channels-automation-run/state + */ + +import type { ContentRef, ErrorInfo, URI, UsageInfo } from '../common/state.js'; + +/** + * 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', +} + +/** + * 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', +} + +/** + * Summary of why a run cannot currently make progress. + * + * @category Automation Run State + */ +export interface AutomationRunBlocker { + /** Category of the outstanding dependency. */ + kind: AutomationRunBlockerKind; +} + +/** + * 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', +} + +/** + * Cause recorded for a client-requested manual run. + * + * @category Automation Run State + */ +export interface AutomationManualRunCause { + kind: AutomationRunCauseKind.Manual; +} + +/** + * 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, non-secret event provenance suitable for display or audit. + * This is descriptive context, not an input that clients replay. + */ + event?: Record; +} + +/** + * Immutable provenance describing why a run was created. + * + * @category Automation Run State + */ +export type AutomationRunCause = + | AutomationManualRunCause + | AutomationTriggeredRunCause; + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * Discriminated lifecycle of an automation run. + * + * @category Automation Run State + */ +export type AutomationRunLifecycle = + | AutomationPendingRunLifecycle + | AutomationRunningRunLifecycle + | AutomationBlockedRunLifecycle + | AutomationCompletedRunLifecycle + | AutomationFailedRunLifecycle + | AutomationCancelledRunLifecycle; + +/** + * Operations the host currently permits for a run. + * + * @category Automation Run State + */ +export const enum AutomationRunOperation { + /** Request cancellation with `automationRun/cancelRequested`. */ + Cancel = 'cancel', +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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 new file mode 100644 index 000000000..0315c6d4f --- /dev/null +++ b/types/channels-automation/actions.ts @@ -0,0 +1,84 @@ +/** + * Automation Channel Actions — server-authored mutations of an + * `ahp-automation:` channel. + * + * @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'; + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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 new file mode 100644 index 000000000..e6f10e53e --- /dev/null +++ b/types/channels-automation/commands.ts @@ -0,0 +1,293 @@ +/** + * Automation Commands — catalogue discovery and mutation of + * `ahp-automation:` resources. + * + * @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'; + +/** + * 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; +} + +/** + * 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; +} + +/** + * Host-defined event trigger types available for the supplied context. + * + * @category Commands + */ +export interface ListAutomationTriggerDefinitionsResult { + /** Available event trigger definitions. */ + items: AutomationTriggerDefinition[]; +} + +/** + * Initial schedule occurrence retained while an imported automation is disabled. + * + * @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 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[]; +} + +/** + * 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; + /** + * Optional legacy import state. When present, {@link definition} MUST be + * disabled so automatic triggers cannot run before migration cutover. + */ + import?: AutomationImport; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; +} + +/** + * Host-canonical future schedule occurrences. + * + * @category Commands + */ +export interface PreviewAutomationScheduleResult { + /** Ascending ISO 8601 timestamps. */ + 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..65dfa2ae3 --- /dev/null +++ b/types/channels-automation/state.ts @@ -0,0 +1,374 @@ +/** + * 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'; +import type { ModelSelection } from '../channels-root/state.js'; +import type { AgentSelection } from '../channels-session/state.js'; + +/** + * 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', +} + +/** + * 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', +} + +/** + * 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 Database identifier used to interpret the expression, for + * example `"UTC"` or `"Europe/Berlin"`. + */ + timeZone: string; +} + +/** + * 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', +} + +/** + * 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', +} + +/** + * Starts runs from a recurring cron schedule evaluated by the host. + * + * @category Automation State + */ +export interface AutomationScheduleTrigger { + /** + * 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; +} + +/** + * 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 { + /** + * Identifier unique and stable within this automation definition. Run causes + * refer back to this value. + */ + id: string; + kind: AutomationTriggerKind.Event; + /** Matches {@link AutomationTriggerDefinition.type}. */ + type: string; + /** + * Selected {@link AutomationTriggerEventDefinition.id | event ids} for this + * trigger type. + */ + events: string[]; + /** + * Values described by {@link AutomationTriggerDefinition.configSchema}. + * Clients MUST preserve unknown entries when editing other fields. + */ + config?: Record; +} + +/** + * 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; + +/** + * 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; +} + +/** + * 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; +} + +/** + * 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; + /** + * Ordered working-directory URIs for each created session. Absence means a + * workspace-less session. + */ + workingDirectories?: URI[]; + /** + * Session configuration values accepted by `createSession`, normally + * obtained from `resolveSessionConfig`. + */ + config?: Record; +} + +/** + * 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 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; + /** + * Whether automatic triggers may create runs. Manual runs remain available + * whenever {@link AutomationOperation.Run} is advertised. + */ + enabled: boolean; + /** 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; +} + +/** + * 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; +} + +/** + * 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 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; + /** 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; +} + +/** + * 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 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 + * `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 eb74fedc5..84642c87d 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,66 @@ export interface SessionSummaryChangedParams { changes: Partial; } +// ─── 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; +} + // ─── progress ──────────────────────────────────────────────────────────────── /** diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index e10c1d221..5eda3e0e7 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -59,6 +59,40 @@ export const enum SessionStatus { IsArchived = 1 << 6, } +/** + * 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', +} + +/** + * 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; +} + +/** + * Durable provenance for sessions created by a higher-level AHP workflow. + * + * @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 +115,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..daa88f32a 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 } from '../channels-automation/state.js'; // ─── BaseParams ────────────────────────────────────────────────────────────── @@ -266,8 +267,100 @@ export interface InitializeResult { * @see {@link /specification/telemetry-channel | Telemetry Channel} */ telemetry?: TelemetryCapabilities; + /** + * 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; +} + +/** + * 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; } +/** + * Automatic trigger execution availability. + * + * @category Commands + */ +export interface AutomationExecutionCapabilities { + /** How long automatic trigger evaluation remains available. */ + lifetime: AutomationExecutionLifetime; +} + +/** + * Presence capability for `createAutomation`. + * + * The empty object means "supported"; fields are reserved for future + * create-specific options. + * + * @category Commands + */ +export interface AutomationCreateCapability {} + +/** + * 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 { + /** + * Smallest permitted interval between consecutive occurrences. Omission + * means no restriction beyond the cron format's one-minute resolution. + */ + minIntervalMinutes?: number; +} + +/** + * 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 {} + +/** + * 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/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/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]; } 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..bbd8e1056 --- /dev/null +++ b/types/test-cases/round-trips/041-automation-snapshot.json @@ -0,0 +1,94 @@ +{ + "name": "automation-snapshot", + "group": "A", + "description": "An automation snapshot preserves cron 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": "morning", + "kind": "schedule", + "schedule": { + "expression": "30 9 * * *", + "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": "morning", + "kind": "schedule", + "schedule": { + "expression": "30 9 * * *", + "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..bf79da727 --- /dev/null +++ b/types/test-cases/round-trips/043-automation-capabilities.json @@ -0,0 +1,36 @@ +{ + "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": { + "minIntervalMinutes": 5 + }, + "runCancellation": {}, + "schedulePreview": {}, + "runHistoryLimit": 50 + } + }, + "acceptableOutputs": [{ + "protocolVersion": "0.8.0", + "serverSeq": 1, + "snapshots": [], + "automations": { + "execution": { "lifetime": "managed" }, + "create": {}, + "schedules": { + "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',