diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 7ae418cd29163..6028796f7b8a5 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -19,11 +19,12 @@ dependencies = [ [[package]] name = "ahp" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0e38f1cb5959958300e43d648624a142becea4fe498ebc62ab11d2bdcdd971" +checksum = "e78bcc1da853fe667b53c71c29d81917081511bf2a4bf299a0020fbacc971b98" dependencies = [ "ahp-types", + "jiff", "serde", "serde_json", "thiserror 2.0.18", @@ -33,9 +34,9 @@ dependencies = [ [[package]] name = "ahp-types" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f698acd3312d94d9f4ce9f2d6c2d3fa778bb088548a288d81c7384c71fe7c23d" +checksum = "cc0674a9fabc13c97d309f1df5041a1e0811fc244200846f61b154660d3209da" dependencies = [ "serde", "serde_json", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7fe5f28010af9..2fa952a91636b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -56,8 +56,8 @@ console = "0.15.7" bytes = "1.11.1" tar = "0.4.46" local-ip-address = "0.6" -ahp = "0.7.0" -ahp-types = "0.7.0" +ahp = "0.9.0" +ahp-types = "0.9.0" [build-dependencies] serde = { version="1.0.163", features = ["derive"] } diff --git a/cli/src/commands/agent.rs b/cli/src/commands/agent.rs index 93d0da513f38e..da13465a3fe5a 100644 --- a/cli/src/commands/agent.rs +++ b/cli/src/commands/agent.rs @@ -366,6 +366,7 @@ async fn authenticate_from_error( resource: resource.resource.clone(), token: credential.access_token().to_string(), scopes: None, + meta: None, }, ) .await diff --git a/cli/src/commands/agent_discovery.rs b/cli/src/commands/agent_discovery.rs index c40b2f133e21b..a82010f4c55c6 100644 --- a/cli/src/commands/agent_discovery.rs +++ b/cli/src/commands/agent_discovery.rs @@ -139,6 +139,7 @@ async fn probe_host( channel: ROOT_RESOURCE_URI.to_string(), limit: None, cursor: None, + meta: None, }, ) .await; diff --git a/cli/src/commands/agent_ps.rs b/cli/src/commands/agent_ps.rs index 97c75fa735838..f367871b69857 100644 --- a/cli/src/commands/agent_ps.rs +++ b/cli/src/commands/agent_ps.rs @@ -123,6 +123,7 @@ async fn list_sessions( channel: ROOT_RESOURCE_URI.to_string(), limit: None, cursor, + meta: None, }, ) .await?; @@ -417,6 +418,7 @@ mod tests { modified_at: modified_at.to_string(), project: None, working_directories: None, + origin: None, changes: None, annotations: None, meta: None, diff --git a/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts b/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts index f2b8bad3ec343..0d1c45a11c7ef 100644 --- a/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts +++ b/src/vs/platform/agentHost/browser/agentHostProtocolClient.ts @@ -54,6 +54,8 @@ import { computeReconnectDelay, DEFAULT_RECONNECT_POLICY, hasExhaustedReconnectA import type { IRemoteAgentHostProtocolClient } from '../common/remoteAgentHostService.js'; const AHP_CLIENT_CONNECTION_CLOSED = -32000; +// AHP 0.9 changed the automation catalog wire shape, so VS Code cannot safely negotiate 0.8. +const CLIENT_SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS.filter(version => version !== '0.8.0'); /** * After this much inbound silence, send an application-level `ping` to @@ -469,10 +471,10 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect const result = await this._dispatchRequest('initialize', { channel: ROOT_STATE_URI, - // Advertise every version this client can negotiate, most-preferred first, so an + // Advertise every compatible version, most-preferred first, so an // older host (a cloud sandbox running a 0.5.x `copilotd`) can negotiate down // instead of rejecting the connection. A current host still picks the newest. - protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS], + protocolVersions: [...CLIENT_SUPPORTED_PROTOCOL_VERSIONS], clientId: this._clientId, clientInfo: this._clientInfo, _meta: this._clientMeta(), @@ -761,7 +763,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect this._logService.info(`[RemoteAgentHostProtocol] Server forgot client ${this._clientId}; initializing a fresh connection.`); const initializeResult = await this._dispatchRequest('initialize', { channel: ROOT_STATE_URI, - protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS], + protocolVersions: [...CLIENT_SUPPORTED_PROTOCOL_VERSIONS], clientId: this._clientId, clientInfo: this._clientInfo, _meta: this._clientMeta(), diff --git a/src/vs/platform/agentHost/common/state/agentSubscription.ts b/src/vs/platform/agentHost/common/state/agentSubscription.ts index 0ae471a3f5662..bdd3576868c84 100644 --- a/src/vs/platform/agentHost/common/state/agentSubscription.ts +++ b/src/vs/platform/agentHost/common/state/agentSubscription.ts @@ -13,7 +13,7 @@ import { ActionEnvelope, ActionType, type AutomationAction, type AutomationRunAc import { automationReducer, automationRunReducer, changesetReducer, chatReducer, annotationsReducer, rootReducer, sessionReducer } from './sessionReducers.js'; import { terminalReducer } from './protocol/reducers.js'; import type { RootAction, SessionAction as IProtocolSessionAction, ChatAction as IProtocolChatAction, TerminalAction } from './protocol/action-origin.generated.js'; -import type { AnnotationsState, AutomationCatalogState, AutomationRunState, ChangesetState, ChatState, RootState, SessionState, TerminalState } from './protocol/state.js'; +import type { AnnotationsState, AutomationRunState, AutomationState, ChangesetState, ChatState, RootState, SessionState, TerminalState } from './protocol/state.js'; import type { IStateSnapshot } from './sessionProtocol.js'; import { isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, isAhpRootChannel, ROOT_STATE_URI, StateComponents } from './sessionState.js'; import { normalizeLegacyChatStateErrors } from './legacyProtocolCompatibility.js'; @@ -577,13 +577,13 @@ export class TerminalStateSubscription extends BaseAgentSubscription { +export class AutomationCatalogSubscription extends BaseAgentSubscription { constructor(clientId: string, log: (msg: string) => void) { super(clientId, log); } - protected override _applyReducer(state: AutomationCatalogState, action: StateAction): AutomationCatalogState { + protected override _applyReducer(state: AutomationState, action: StateAction): AutomationState { return automationReducer(state, action as AutomationAction, this._log); } @@ -1254,6 +1254,14 @@ export function isActionEnvelopeRelevantToSubscriptionUris(envelope: ActionEnvel } return false; } + if (isAhpAutomationCatalogChannel(envelope.channel)) { + for (const uri of subscribedUris) { + if (isAhpAutomationCatalogChannel(uri)) { + return true; + } + } + return false; + } for (const uri of subscribedUris) { if (uri === envelope.channel) { return true; diff --git a/src/vs/platform/agentHost/common/state/protocol/.ahp-version b/src/vs/platform/agentHost/common/state/protocol/.ahp-version index 1519ab5e09231..6110301d110ec 100644 --- a/src/vs/platform/agentHost/common/state/protocol/.ahp-version +++ b/src/vs/platform/agentHost/common/state/protocol/.ahp-version @@ -1 +1 @@ -a0bc67f8 +60706330 diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-automation-run/state.ts b/src/vs/platform/agentHost/common/state/protocol/channels-automation-run/state.ts index c3ca0a976ee52..0b3a920c067e1 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-automation-run/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-automation-run/state.ts @@ -7,7 +7,7 @@ // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts import type { ErrorInfo, URI, UsageInfo } from '../common/state.js'; -import type { AutomationEventTrigger, AutomationMisfirePolicy, AutomationScheduleTrigger, AutomationState } from '../channels-automation/state.js'; +import type { AutomationEventTrigger, AutomationMisfirePolicy, AutomationScheduleTrigger, AutomationEntry } from '../channels-automation/state.js'; import type { RunAutomationParams } from '../channels-automation/commands.js'; import type { SessionState } from '../channels-session/state.js'; @@ -227,7 +227,7 @@ export interface AutomationRunSummary { export interface AutomationRunState { /** URI of this automation-run channel. */ resource: URI; - /** Owning `ahp-automation:` URI matching {@link AutomationState.resource}. */ + /** Owning `ahp-automation:` URI matching {@link AutomationEntry.resource}. */ automation: URI; /** Immutable provenance describing how this run was created. */ origin: AutomationRunOrigin; diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-automation/actions.ts b/src/vs/platform/agentHost/common/state/protocol/channels-automation/actions.ts index 9dbafdb743c14..f4e2a71346cf7 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-automation/actions.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-automation/actions.ts @@ -9,7 +9,7 @@ import { ActionType } from '../common/actions.js'; import type { Message } from '../channels-chat/state.js'; import type { URI } from '../common/state.js'; -import type { AutomationCatalogState, AutomationDefinition, AutomationOperation, AutomationSessionTemplate, AutomationState, AutomationTrigger } from './state.js'; +import type { AutomationDefinition, AutomationEntry, AutomationOperation, AutomationSessionTemplate, AutomationState, AutomationTrigger } from './state.js'; /** * Partial replacement of editable {@link AutomationDefinition} fields. @@ -60,9 +60,9 @@ export interface AutomationDefinitionPatch { */ export interface AutomationCreateRequestedAction { type: ActionType.AutomationCreateRequested; - /** Client-chosen `ahp-automation:` URI that becomes {@link AutomationState.resource}. */ + /** Client-chosen `ahp-automation:` URI that becomes {@link AutomationEntry.resource}. */ resource: URI; - /** Complete initial {@link AutomationState.definition}. */ + /** Complete initial {@link AutomationEntry.definition}. */ definition: AutomationDefinition; } @@ -87,7 +87,7 @@ export interface AutomationCreateRequestedAction { */ export interface AutomationUpdateRequestedAction { type: ActionType.AutomationUpdateRequested; - /** Target {@link AutomationState.resource}. */ + /** Target {@link AutomationEntry.resource}. */ resource: URI; /** Editable {@link AutomationDefinition} fields to replace. */ changes: AutomationDefinitionPatch; @@ -95,9 +95,9 @@ export interface AutomationUpdateRequestedAction { /** * Add or replace one full automation state in - * {@link AutomationCatalogState.automations}. + * {@link AutomationState.entries}. * - * Existing entries are matched by {@link AutomationState.resource} and + * Existing entries are matched by {@link AutomationEntry.resource} and * replaced in place. A previously unseen resource is appended. * * @category Automation Actions @@ -106,11 +106,11 @@ export interface AutomationUpdateRequestedAction { export interface AutomationSetAction { type: ActionType.AutomationSet; /** Full new or replacement automation state. */ - automation: AutomationState; + automation: AutomationEntry; } /** - * Remove one automation from {@link AutomationCatalogState.automations}. + * Remove one automation from {@link AutomationState.entries}. * * Clients may dispatch this action only while the target advertises * {@link AutomationOperation.Remove}. The host revalidates that operation @@ -125,6 +125,6 @@ export interface AutomationSetAction { */ export interface AutomationRemovedAction { type: ActionType.AutomationRemoved; - /** {@link AutomationState.resource} to remove. */ + /** {@link AutomationEntry.resource} to remove. */ resource: URI; } diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-automation/commands.ts b/src/vs/platform/agentHost/common/state/protocol/channels-automation/commands.ts index e7aec7c2fbc28..780382e53ffdd 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-automation/commands.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-automation/commands.ts @@ -11,7 +11,7 @@ import type { URI } from '../common/state.js'; import type { AutomationRunState } from '../channels-automation-run/state.js'; import type { AgentInfo } from '../channels-root/state.js'; import type { AutomationSetAction } from './actions.js'; -import type { AutomationDefinition, AutomationSessionTemplate, AutomationState, AutomationTriggerDefinition } from './state.js'; +import type { AutomationDefinition, AutomationEntry, AutomationSessionTemplate, AutomationTriggerDefinition } from './state.js'; /** * Discover event-trigger types available for a prospective session template. @@ -63,8 +63,8 @@ export interface ListAutomationTriggerDefinitionsResult { */ export interface RunAutomationParams extends BaseParams { /** Manual runs are scoped to the catalogue channel. */ - channel: 'ahp-automations://catalog'; - /** Target {@link AutomationState.resource}. */ + channel: 'ahp-automations://'; + /** Target {@link AutomationEntry.resource}. */ automation: URI; /** * Durable client-generated idempotency key. Retrying with the same key and @@ -89,7 +89,7 @@ export interface RunAutomationResult { * * The response only acknowledges the request. The updated full state arrives * through {@link AutomationSetAction | `automation/set`} on the - * `ahp-automations://catalog` channel, keeping all catalogue subscribers synchronized + * `ahp-automations://` channel, keeping all catalogue subscribers synchronized * through the normal action stream. * * @category Commands @@ -100,11 +100,11 @@ export interface RunAutomationResult { */ export interface FetchAutomationRunsParams extends BaseParams { /** Run-history loading is scoped to the catalogue channel. */ - channel: 'ahp-automations://catalog'; - /** Target {@link AutomationState.resource}. */ + channel: 'ahp-automations://'; + /** Target {@link AutomationEntry.resource}. */ automation: URI; /** - * Cursor previously received as {@link AutomationState.runsNextCursor}. + * Cursor previously received as {@link AutomationEntry.runsNextCursor}. * Omit to request the first page not already included by the snapshot. */ cursor?: string; diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-automation/reducer.ts b/src/vs/platform/agentHost/common/state/protocol/channels-automation/reducer.ts index a02efe823b1e4..5372b8c458bd0 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-automation/reducer.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-automation/reducer.ts @@ -9,36 +9,36 @@ import type { AutomationAction } from '../action-origin.generated.js'; import { ActionType } from '../common/actions.js'; import { softAssertNever } from '../common/reducer-helpers.js'; -import type { AutomationCatalogState } from './state.js'; +import type { AutomationState } from './state.js'; /** Pure reducer for automation catalogue state. */ -export function automationReducer(state: AutomationCatalogState, action: AutomationAction, log?: (msg: string) => void): AutomationCatalogState { +export function automationReducer(state: AutomationState, action: AutomationAction, log?: (msg: string) => void): AutomationState { switch (action.type) { case ActionType.AutomationCreateRequested: case ActionType.AutomationUpdateRequested: return state; case ActionType.AutomationSet: { - const idx = state.automations.findIndex(automation => automation.resource === action.automation.resource); + const idx = state.entries.findIndex(automation => automation.resource === action.automation.resource); if (idx < 0) { return { ...state, - automations: [...state.automations, action.automation], + entries: [...state.entries, action.automation], }; } - const automations = state.automations.slice(); - automations[idx] = action.automation; - return { ...state, automations }; + const entries = state.entries.slice(); + entries[idx] = action.automation; + return { ...state, entries }; } case ActionType.AutomationRemoved: { - const idx = state.automations.findIndex(automation => automation.resource === action.resource); + const idx = state.entries.findIndex(automation => automation.resource === action.resource); if (idx < 0) { return state; } - const automations = state.automations.slice(); - automations.splice(idx, 1); - return { ...state, automations }; + const entries = state.entries.slice(); + entries.splice(idx, 1); + return { ...state, entries }; } default: diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-automation/state.ts b/src/vs/platform/agentHost/common/state/protocol/channels-automation/state.ts index 95f2455f81e20..67b86d3eb8480 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-automation/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-automation/state.ts @@ -19,7 +19,7 @@ import type { FetchAutomationRunsParams, ListAutomationTriggerDefinitionsParams, /** * Operations the host currently permits for an automation. * - * The list on {@link AutomationState.operations} is authoritative and may + * The list on {@link AutomationEntry.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. @@ -255,7 +255,7 @@ export interface AutomationSessionTemplate { * A definition combines the initial automation message, the session template * used for each run, and zero or more automatic triggers. Run history, * timestamps, and currently allowed operations live on - * {@link AutomationState} rather than in the definition. + * {@link AutomationEntry} rather than in the definition. * * @category Automation State */ @@ -284,8 +284,7 @@ export interface AutomationDefinition { } /** - * Authoritative state of one automation in the - * {@link AutomationCatalogState.automations} catalogue. + * Authoritative state of one automation in {@link AutomationState.entries}. * * The host owns trigger evaluation, run claims, run retention, and operation * availability. Clients render this state and submit actions or commands; they @@ -293,7 +292,7 @@ export interface AutomationDefinition { * * @category Automation State */ -export interface AutomationState { +export interface AutomationEntry { /** Stable `ahp-automation:/` resource identifier. */ resource: URI; /** Current durable definition. */ @@ -303,7 +302,7 @@ export interface AutomationState { /** * Newest-first retained run summaries. This is a bounded window; use * {@link FetchAutomationRunsParams | fetchAutomationRuns} when - * {@link AutomationState.runsNextCursor} is present. + * {@link AutomationEntry.runsNextCursor} is present. */ runs: AutomationRunSummary[]; /** Opaque cursor passed as {@link FetchAutomationRunsParams.cursor} for the next older run-history page. */ @@ -319,7 +318,7 @@ export interface AutomationState { } /** - * Authoritative automation catalogue exposed on the `ahp-automations://catalog` + * Authoritative automation catalogue exposed on the `ahp-automations://` * channel. * * A subscription snapshot contains every automation visible to the client. @@ -329,9 +328,9 @@ export interface AutomationState { * * @category Automation State */ -export interface AutomationCatalogState { - /** Full automation states keyed by {@link AutomationState.resource}. */ - automations: AutomationState[]; +export interface AutomationState { + /** Full automation entries keyed by {@link AutomationEntry.resource}. */ + entries: AutomationEntry[]; /** Opaque host-defined catalogue metadata. */ _meta?: Record; } diff --git a/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts b/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts index 4a15a7e80dee7..445f33494ce5b 100644 --- a/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/channels-session/state.ts @@ -10,7 +10,7 @@ import type { Changeset } from '../channels-changeset/state.js'; import type { AnnotationsSummary } from '../channels-annotations/state.js'; import type { ChatSummary, ChatInputRequest, ToolCallConfirmationState, ToolCallRunningState, ToolCallAuthRequiredState } from '../channels-chat/state.js'; import type { AutomationRunState } from '../channels-automation-run/state.js'; -import type { AutomationState } from '../channels-automation/state.js'; +import type { AutomationEntry } from '../channels-automation/state.js'; import type { ConfigPropertySchema, ErrorInfo, Icon, ProtectedResourceMetadata, TextRange, URI } from '../common/state.js'; // ─── Session State ─────────────────────────────────────────────────────────── @@ -74,7 +74,7 @@ export const enum SessionOriginKind { */ export interface AutomationSessionOrigin { kind: SessionOriginKind.Automation; - /** Owning {@link AutomationState.resource}. */ + /** Owning {@link AutomationEntry.resource}. */ automation: URI; /** Owning {@link AutomationRunState.resource}. */ run: URI; diff --git a/src/vs/platform/agentHost/common/state/protocol/common/commands.ts b/src/vs/platform/agentHost/common/state/protocol/common/commands.ts index 05000eb38ec5d..5ac6b4778a521 100644 --- a/src/vs/platform/agentHost/common/state/protocol/common/commands.ts +++ b/src/vs/platform/agentHost/common/state/protocol/common/commands.ts @@ -10,7 +10,7 @@ import type { URI, Snapshot } from './state.js'; import type { ActionEnvelope, StateAction } from './actions.js'; import type { AutomationRunCancelRequestedAction } from '../channels-automation-run/actions.js'; import type { AutomationCreateRequestedAction } from '../channels-automation/actions.js'; -import type { AutomationSchedule, AutomationScheduleTrigger, AutomationCatalogState, AutomationState } from '../channels-automation/state.js'; +import type { AutomationSchedule, AutomationScheduleTrigger, AutomationEntry, AutomationState } from '../channels-automation/state.js'; import type { TelemetryCapabilities } from '../channels-otlp/state.js'; // ─── BaseParams ────────────────────────────────────────────────────────────── @@ -281,7 +281,7 @@ export interface InitializeResult { telemetry?: TelemetryCapabilities; /** * Host-owned automation support. Presence means clients may subscribe to - * `ahp-automations://catalog` for {@link AutomationCatalogState}; absence means the + * `ahp-automations://` for {@link AutomationState}; absence means the * host does not expose an automation catalogue or automation commands. * * @see {@link /guide/automations | Automations Guide} @@ -292,12 +292,12 @@ export interface InitializeResult { /** * Automation features supported by this host authority. * - * The presence of this object advertises the baseline `ahp-automations://catalog` + * The presence of this object advertises the baseline `ahp-automations://` * catalogue. Optional fields describe additional host features and * restrictions. * * Capabilities describe implementation support. - * {@link AutomationState.operations} remains authoritative for which + * {@link AutomationEntry.operations} remains authoritative for which * definition mutations are currently allowed on a particular automation. * * @category Commands @@ -313,7 +313,7 @@ export interface AutomationCapabilities { */ runCancellation?: AutomationRunCancellationCapability; /** - * Maximum terminal entries retained in {@link AutomationState.runs}. Active + * Maximum terminal entries retained in {@link AutomationEntry.runs}. Active * runs are not counted toward the limit. Absence means the retention limit is * implementation-defined. */ diff --git a/src/vs/platform/agentHost/common/state/protocol/common/state.ts b/src/vs/platform/agentHost/common/state/protocol/common/state.ts index d8b8db833146c..14df0f4718f83 100644 --- a/src/vs/platform/agentHost/common/state/protocol/common/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/common/state.ts @@ -13,7 +13,7 @@ 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 { AutomationCatalogState } from '../channels-automation/state.js'; +import type { AutomationState } from '../channels-automation/state.js'; import type { AutomationRunState } from '../channels-automation-run/state.js'; // ─── Type Aliases ──────────────────────────────────────────────────────────── @@ -334,7 +334,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 | AutomationCatalogState | AutomationRunState; + 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/src/vs/platform/agentHost/common/state/protocol/version/registry.ts b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts index cda0b8fa5b32f..86a7dfeb64226 100644 --- a/src/vs/platform/agentHost/common/state/protocol/version/registry.ts +++ b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts @@ -16,7 +16,7 @@ import type { ServerNotificationMap } from '../messages.js'; * * Formatted as a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH` string. */ -export const PROTOCOL_VERSION = '1.0.0'; +export const PROTOCOL_VERSION = '0.9.0'; /** * Every protocol version a client built from this source tree is willing @@ -35,7 +35,7 @@ export const PROTOCOL_VERSION = '1.0.0'; * `scripts/verify-release-metadata.ts`. */ export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([ - '1.0.0', + '0.9.0', '0.8.0', '0.7.0', '0.6.0', @@ -126,7 +126,7 @@ export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string [ActionType.ChatTurnComplete]: '0.4.0', [ActionType.ChatTurnCancelled]: '0.4.0', [ActionType.ChatError]: '0.4.0', - [ActionType.ChatTurnResume]: '1.0.0', + [ActionType.ChatTurnResume]: '0.9.0', [ActionType.ChatActivityChanged]: '0.5.0', [ActionType.ChatWorkingDirectorySet]: '0.7.0', [ActionType.ChatWorkingDirectoryRemoved]: '0.7.0', diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index b9c01605ce92e..31dcd0d386377 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -37,7 +37,7 @@ import { type PendingMessage, type Turn, type AnnotationsState, - type AutomationCatalogState, + type AutomationState, type AutomationRunState, type URI as ProtocolURI, type RootState, @@ -76,7 +76,7 @@ export { type MessageResourceAttachment, type MessageEmbeddedResourceAttachment, type MessageAnnotationsAttachment, type MessageChatAttachment, type ModelSelection, type PendingMessage, type PluginCustomization, type ProjectInfo, type PromptCustomization, type ReasoningResponsePart, type ErrorResponsePart, type ResponsePart, type RootState, type RuleCustomization, type SessionActiveClient, - type AutomationCatalogState, type AutomationRunState, + type AutomationState, type AutomationRunState, type SessionConfigState, type SessionModelInfo, type SessionState, @@ -249,18 +249,23 @@ export interface UsageInfoMeta { /** * Singleton channel containing the host-owned automation catalogue. - * - * The `catalog` authority is appended so the URI round-trips through - * `.toString()`. Without an authority, `ahp-automations://` serializes back to - * `ahp-automations:` and no longer matches. Comparing catalogue channels as - * URIs everywhere (ResourceMap/isEqual) is the intended followup. See - * https://github.com/microsoft/vscode/pull/331796#discussion_r3857160917. */ -export const AUTOMATION_CATALOG_URI = 'ahp-automations://catalog'; +export const AHP_AUTOMATIONS_SCHEME = 'ahp-automations'; +export const AUTOMATION_CATALOG_URI = `${AHP_AUTOMATIONS_SCHEME}://`; -/** Returns whether `uri` identifies the singleton automation catalogue channel. */ +/** + * Returns whether `uri` identifies the singleton automation catalogue channel, + * including forms normalized by the workbench {@link ResourceURI} class. + */ export function isAhpAutomationCatalogChannel(uri: string): boolean { - return uri === AUTOMATION_CATALOG_URI; + if (uri === AUTOMATION_CATALOG_URI) { + return true; + } + try { + return ResourceURI.parse(uri).scheme === AHP_AUTOMATIONS_SCHEME; + } catch { + return false; + } } /** Returns whether `uri` identifies one automation-run channel. */ @@ -1068,7 +1073,7 @@ export type ComponentToState = { [StateComponents.Terminal]: TerminalState; [StateComponents.Changeset]: ChangesetState; [StateComponents.Annotations]: AnnotationsState; - [StateComponents.AutomationCatalog]: AutomationCatalogState; + [StateComponents.AutomationCatalog]: AutomationState; [StateComponents.AutomationRun]: AutomationRunState; }; diff --git a/src/vs/platform/agentHost/node/agentHostAutomationService.ts b/src/vs/platform/agentHost/node/agentHostAutomationService.ts index 18bd7a7ae275b..6e98e27f32ebf 100644 --- a/src/vs/platform/agentHost/node/agentHostAutomationService.ts +++ b/src/vs/platform/agentHost/node/agentHostAutomationService.ts @@ -13,11 +13,11 @@ import { localize } from '../../../nls.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ILogService } from '../../log/common/log.js'; import { ActionType, type ActionEnvelope, type AutomationCreateRequestedAction, type AutomationRemovedAction, type AutomationRunCancelRequestedAction, type AutomationRunLifecycleChangedAction, type AutomationRunPrimarySessionChangedAction, type AutomationRunSessionSetAction, type AutomationUpdateRequestedAction } from '../common/state/sessionActions.js'; -import { AUTOMATION_CATALOG_URI, isDefaultChatUri, parseRequiredSessionUriFromChatUri, type AutomationCatalogState, type Message } from '../common/state/sessionState.js'; +import { AUTOMATION_CATALOG_URI, isDefaultChatUri, parseRequiredSessionUriFromChatUri, type AutomationState, type Message } from '../common/state/sessionState.js'; import { automationReducer } from '../common/state/sessionReducers.js'; import type { AutomationCapabilities } from '../common/state/protocol/common/commands.js'; import type { FetchAutomationRunsParams, FetchAutomationRunsResult, ListAutomationTriggerDefinitionsParams, ListAutomationTriggerDefinitionsResult, RunAutomationParams, RunAutomationResult } from '../common/state/protocol/channels-automation/commands.js'; -import { AutomationMisfirePolicy, AutomationOperation, AutomationTriggerKind, type AutomationDefinition, type AutomationSessionTemplate, type AutomationState } from '../common/state/protocol/channels-automation/state.js'; +import { AutomationMisfirePolicy, AutomationOperation, AutomationTriggerKind, type AutomationDefinition, type AutomationEntry, type AutomationSessionTemplate } from '../common/state/protocol/channels-automation/state.js'; import { AutomationRunOriginKind, AutomationRunStatus, type AutomationRunLifecycle, type AutomationRunOrigin, type AutomationRunState, type AutomationRunSummary } from '../common/state/protocol/channels-automation-run/state.js'; import { MessageKind } from '../common/state/protocol/channels-chat/state.js'; import { IAgentHostStateManager, type AgentHostStateManager } from './agentHostStateManager.js'; @@ -38,9 +38,14 @@ interface IStoredManualRunRequest { readonly run: string; } +interface IStoredAutomationCatalog { + readonly automations: readonly AutomationEntry[]; + readonly _meta?: Record; +} + interface IStoredAutomations { readonly version?: 1; - readonly catalog: AutomationCatalogState; + readonly catalog: IStoredAutomationCatalog; readonly runs?: readonly AutomationRunState[]; readonly manualRunRequests?: readonly IStoredManualRunRequest[]; readonly migration?: { @@ -82,7 +87,7 @@ export interface IAgentHostAutomationService { export class AgentHostAutomationService extends Disposable implements IAgentHostAutomationService { declare readonly _serviceBrand: undefined; - private _catalog: AutomationCatalogState | undefined; + private _catalog: AutomationState | undefined; private _migrationCompletedAt: string | undefined; private _runs = new Map(); private _manualRunRequests = new Map(); @@ -102,9 +107,13 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost this._migrationCompletedAt = stored?.migration?.completedAt; this._runs = new Map(stored?.runs?.map(run => [run.resource, run])); this._catalog = stored?.catalog ? { - ...stored.catalog, - automations: stored.catalog.automations.map(automation => withRunWindow(automation, this._runs, RUN_HISTORY_PAGE_SIZE)), - ...(this._migrationCompletedAt ? { _meta: { ...stored.catalog._meta, [AGENT_HOST_AUTOMATION_CATALOG_MIGRATED_META_KEY]: true } } : {}), + entries: stored.catalog.automations.map(automation => withRunWindow(automation, this._runs, RUN_HISTORY_PAGE_SIZE)), + ...(stored.catalog._meta || this._migrationCompletedAt ? { + _meta: { + ...stored.catalog._meta, + ...(this._migrationCompletedAt ? { [AGENT_HOST_AUTOMATION_CATALOG_MIGRATED_META_KEY]: true } : {}), + }, + } : {}), } : undefined; this._manualRunRequests = new Map(stored?.manualRunRequests?.map(request => [request.requestId, request])); if (this._catalog) { @@ -144,8 +153,8 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost if (this._migrationCompletedAt !== undefined) { return; } - const missing = (expectedResources ?? catalog.automations.map(automation => automation.resource)) - .filter(resource => !catalog.automations.some(automation => automation.resource === resource)); + const missing = (expectedResources ?? catalog.entries.map(automation => automation.resource)) + .filter(resource => !catalog.entries.some(automation => automation.resource === resource)); if (missing.length > 0) { throw new Error(`Automation migration is incomplete; ${missing.length} expected automation resources are missing.`); } @@ -155,12 +164,12 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost // fails to preserve the pre-migration invariants. const priorCompletedAt = this._migrationCompletedAt; this._migrationCompletedAt = completedAt; - let migratedCatalog: AutomationCatalogState; + let migratedCatalog: AutomationState; try { migratedCatalog = { ...catalog, _meta: { ...catalog._meta, [AGENT_HOST_AUTOMATION_CATALOG_MIGRATED_META_KEY]: true }, - automations: catalog.automations.map(automation => ({ + entries: catalog.entries.map(automation => ({ ...automation, operations: this._migrationOperationsForItem(automation), })), @@ -172,16 +181,16 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost } this._catalog = migratedCatalog; this._stateManager.setAutomationCatalogState(migratedCatalog); - for (const automation of migratedCatalog.automations) { + for (const automation of migratedCatalog.entries) { this._stateManager.dispatchServerAction(AUTOMATION_CATALOG_URI, { type: ActionType.AutomationSet, automation }); } - this._logService.info(`[AgentHostAutomationService] Automation migration completed: discovered=${expectedResources?.length ?? migratedCatalog.automations.length}, automations=${migratedCatalog.automations.length}, runs=${this._runs.size}.`); + this._logService.info(`[AgentHostAutomationService] Automation migration completed: discovered=${expectedResources?.length ?? migratedCatalog.entries.length}, automations=${migratedCatalog.entries.length}, runs=${this._runs.size}.`); this._recoverRuns(); this._scheduleNext(); }); } - private _migrationOperationsForItem(automation: AutomationState): AutomationOperation[] { + private _migrationOperationsForItem(automation: AutomationEntry): AutomationOperation[] { if (!this._canGrantRun(automation.definition)) { // Pending imports or disabled automations must not receive Run or // Remove: the browser scheduler still owns the legacy row until the @@ -202,9 +211,9 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost async handleConfigurationChanged(): Promise { return this._enqueueMutation(async () => { const catalog = this._requireCatalog(); - const nextCatalog: AutomationCatalogState = { + const nextCatalog: AutomationState = { ...catalog, - automations: catalog.automations.map(automation => ({ + entries: catalog.entries.map(automation => ({ ...automation, operations: this._canGrantRun(automation.definition) ? withOperation(automation.operations, AutomationOperation.Run) @@ -215,7 +224,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost if (!equals(nextCatalog, catalog)) { await this._persist(nextCatalog, this._runs, this._manualRunRequests); this._catalog = nextCatalog; - for (const automation of nextCatalog.automations) { + for (const automation of nextCatalog.entries) { this._stateManager.dispatchServerAction(AUTOMATION_CATALOG_URI, { type: ActionType.AutomationSet, automation }); } } @@ -245,7 +254,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost this._validateAutomationResource(action.resource); const definition = action.definition; this._validateDefinition(definition); - const existing = catalog.automations.find(automation => automation.resource === action.resource); + const existing = catalog.entries.find(automation => automation.resource === action.resource); if (existing && equals(existing.definition, definition)) { this._stateManager.dispatchServerAction(AUTOMATION_CATALOG_URI, { type: ActionType.AutomationSet, automation: existing }); return; @@ -281,13 +290,13 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost private async _handleUpdate(action: AutomationUpdateRequestedAction): Promise { const catalog = this._requireCatalog(); - const existing = catalog.automations.find(automation => automation.resource === action.resource); + const existing = catalog.entries.find(automation => automation.resource === action.resource); if (!existing) { throw new Error(`Automation not found: ${action.resource}`); } this._requireOperation(existing, AutomationOperation.Update); - let automation: AutomationState = { + let automation: AutomationEntry = { ...existing, definition: { ...existing.definition, @@ -330,7 +339,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost private async _handleRemove(action: AutomationRemovedAction): Promise { const catalog = this._requireCatalog(); - const existing = catalog.automations.find(automation => automation.resource === action.resource); + const existing = catalog.entries.find(automation => automation.resource === action.resource); if (!existing) { return; } @@ -364,7 +373,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost private async _fetchAutomationRuns(params: FetchAutomationRunsParams): Promise { const catalog = this._requireAvailableCatalog(); - const automation = catalog.automations.find(candidate => candidate.resource === params.automation); + const automation = catalog.entries.find(candidate => candidate.resource === params.automation); if (!automation) { throw new Error(`Automation not found: ${params.automation}`); } @@ -424,28 +433,31 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost } private async _persist( - catalog: AutomationCatalogState, + catalog: AutomationState, runs: ReadonlyMap, manualRunRequests: ReadonlyMap, migrationCompletedAt = this._migrationCompletedAt, ): Promise { await this._storageService.setAndFlush(STORAGE_KEY, { version: 1, - catalog, + catalog: { + automations: catalog.entries, + ...(catalog._meta ? { _meta: catalog._meta } : {}), + }, runs: [...runs.values()], manualRunRequests: [...manualRunRequests.values()], ...(migrationCompletedAt ? { migration: { status: 'complete', completedAt: migrationCompletedAt } } : {}), }); } - private _requireCatalog(): AutomationCatalogState { + private _requireCatalog(): AutomationState { if (!this._catalog) { throw new Error('Automation storage is unavailable and must be recovered before automations can run.'); } return this._catalog; } - private _requireAvailableCatalog(): AutomationCatalogState { + private _requireAvailableCatalog(): AutomationState { const catalog = this._requireCatalog(); if (this._migrationCompletedAt === undefined) { throw new Error('Automation migration must complete before automations can be accessed or run.'); @@ -460,7 +472,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost return this._stateManager.rootState.config?.values[AGENT_HOST_AUTOMATIONS_ENABLED_CONFIG_KEY] === true; } - private _withInitialScheduleState(automation: AutomationState, now: Date): AutomationState { + private _withInitialScheduleState(automation: AutomationEntry, now: Date): AutomationEntry { const cursors: Record = {}; if (automation.definition.enabled) { for (const trigger of automation.definition.triggers) { @@ -481,7 +493,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost if (!this._migrationCompletedAt || !this._catalog || !this._isAutomationsEnabled()) { return; } - const timestamps = this._catalog.automations + const timestamps = this._catalog.entries .filter(automation => automation.definition.enabled && automation.operations.includes(AutomationOperation.Run) && automation.nextRunAt @@ -513,10 +525,10 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost const createdAt = now.toISOString(); let nextCatalog = catalog; const nextRuns = new Map(this._runs); - const changed = new Map(); + const changed = new Map(); const claimed: { run: AutomationRunState; definition: AutomationDefinition }[] = []; - for (const current of catalog.automations) { + for (const current of catalog.entries) { if (!current.definition.enabled) { continue; } @@ -563,7 +575,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost } cursors[trigger.id] = scheduledFor.toISOString(); } - const nextAutomation: AutomationState = { + const nextAutomation: AutomationEntry = { ...automation, nextRunAt: earliestCursor(cursors), _meta: withScheduleCursors(automation._meta, cursors), @@ -610,7 +622,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost if (run.lifecycle.status !== AutomationRunStatus.Pending) { continue; } - const automation = this._catalog?.automations.find(candidate => candidate.resource === run.automation); + const automation = this._catalog?.entries.find(candidate => candidate.resource === run.automation); if (automation && automation.operations.includes(AutomationOperation.Run) && this._execution.isSessionTemplateAvailable(automation.definition.session)) { @@ -636,7 +648,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost return { run: previousRun }; } - const automation = catalog.automations.find(candidate => candidate.resource === params.automation); + const automation = catalog.entries.find(candidate => candidate.resource === params.automation); if (!automation) { throw new Error(`Automation not found: ${params.automation}`); } @@ -852,8 +864,8 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost } } - private _catalogWithRun(catalog: AutomationCatalogState, run: AutomationRunState): AutomationCatalogState { - const existing = catalog.automations.find(automation => automation.resource === run.automation); + private _catalogWithRun(catalog: AutomationState, run: AutomationRunState): AutomationState { + const existing = catalog.entries.find(automation => automation.resource === run.automation); if (!existing) { throw new Error(`Automation not found for run: ${run.automation}`); } @@ -863,8 +875,8 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost return automationReducer(catalog, { type: ActionType.AutomationSet, automation }, this._log); } - private _publishAutomation(catalog: AutomationCatalogState, resource: string): void { - const automation = catalog.automations.find(candidate => candidate.resource === resource); + private _publishAutomation(catalog: AutomationState, resource: string): void { + const automation = catalog.entries.find(candidate => candidate.resource === resource); if (automation) { this._stateManager.dispatchServerAction(AUTOMATION_CATALOG_URI, { type: ActionType.AutomationSet, automation }); } @@ -896,7 +908,7 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost } } - private _requireOperation(automation: AutomationState, operation: AutomationOperation): void { + private _requireOperation(automation: AutomationEntry, operation: AutomationOperation): void { if (!automation.operations.includes(operation)) { throw new Error(`Automation operation '${operation}' is not available: ${automation.resource}`); } @@ -936,12 +948,16 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost private readonly _log = (message: string) => this._logService.warn(`[AgentHostAutomationService] ${message}`); } -function isAutomationCatalogState(value: unknown): value is AutomationCatalogState { +function isStoredAutomationCatalog(value: unknown): value is IStoredAutomationCatalog { if (!value || typeof value !== 'object' || Array.isArray(value)) { return false; } - const automations = (value as Record)['automations']; - return Array.isArray(automations) && automations.every(isAutomationState); + const catalog = value as Record; + const automations = catalog['automations']; + const meta = catalog['_meta']; + return Array.isArray(automations) + && automations.every(isAutomationEntry) + && (meta === undefined || !!meta && typeof meta === 'object' && !Array.isArray(meta)); } function isStoredAutomations(value: unknown): value is IStoredAutomations { @@ -950,13 +966,13 @@ function isStoredAutomations(value: unknown): value is IStoredAutomations { } const stored = value as Record; return (stored['version'] === undefined || stored['version'] === 1) - && isAutomationCatalogState(stored['catalog']) + && isStoredAutomationCatalog(stored['catalog']) && (stored['runs'] === undefined || Array.isArray(stored['runs']) && stored['runs'].every(isAutomationRunState)) && (stored['manualRunRequests'] === undefined || Array.isArray(stored['manualRunRequests']) && stored['manualRunRequests'].every(isStoredManualRunRequest)) && (stored['migration'] === undefined || isCompletedMigration(stored['migration'])); } -function isAutomationState(value: unknown): value is AutomationState { +function isAutomationEntry(value: unknown): value is AutomationEntry { if (!value || typeof value !== 'object' || Array.isArray(value)) { return false; } @@ -1013,7 +1029,7 @@ function toRunSummary(run: AutomationRunState): AutomationRunSummary { }; } -function withRunSummary(automation: AutomationState, allRuns: ReadonlyMap): AutomationState { +function withRunSummary(automation: AutomationEntry, allRuns: ReadonlyMap): AutomationEntry { const terminalLimit = Math.max(RUN_HISTORY_PAGE_SIZE, automation.runs.filter(candidate => isTerminalLifecycle(candidate.lifecycle)).length); const window = withRunWindow(automation, allRuns, terminalLimit); const runs = window.runs; @@ -1026,7 +1042,7 @@ function withRunSummary(automation: AutomationState, allRuns: ReadonlyMap, terminalLimit: number): AutomationState { +function withRunWindow(automation: AutomationEntry, allRuns: ReadonlyMap, terminalLimit: number): AutomationEntry { const summaries = [...allRuns.values()] .filter(run => run.automation === automation.resource) .map(toRunSummary) diff --git a/src/vs/platform/agentHost/node/agentHostStateManager.ts b/src/vs/platform/agentHost/node/agentHostStateManager.ts index e6720c8cd2f02..a000fabbf8c5a 100644 --- a/src/vs/platform/agentHost/node/agentHostStateManager.ts +++ b/src/vs/platform/agentHost/node/agentHostStateManager.ts @@ -13,7 +13,7 @@ import { TelemetryLevel } from '../../telemetry/common/telemetry.js'; import { ActionType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, ChatAction, RootAction, StateAction, TerminalAction, ChangesetAction, ClientChangesetAction, AnnotationsAction, ClientAnnotationsAction, isRootAction, isSessionAction, isChatAction, isChangesetAction, isAnnotationsAction, isAutomationAction, isAutomationRunAction, isPassiveSessionMetadataAction, type AuthRequiredParams, type ClientAutomationAction, type ClientAutomationRunAction, type ProgressParams, type SessionSummaryChangedParams } from '../common/state/sessionActions.js'; import type { IStateSnapshot } from '../common/state/sessionProtocol.js'; import { rootReducer, sessionReducer, chatReducer, changesetReducer, annotationsReducer, automationReducer, automationRunReducer } from '../common/state/sessionReducers.js'; -import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseSubagentSessionUri, isAhpChatChannel, isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, isDefaultChatUri, mergeSessionWithDefaultChat, isAhpRootChannel, readSessionExternal, SessionLifecycle, withHostBuildInfo, withSessionStatusFlag, type AutomationCatalogState, type AutomationRunState, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type Customization, type ISessionWithDefaultChat, type Message, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, IHostBuildInfo, SessionStatus } from '../common/state/sessionState.js'; +import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseSubagentSessionUri, isAhpChatChannel, isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, isDefaultChatUri, mergeSessionWithDefaultChat, isAhpRootChannel, readSessionExternal, SessionLifecycle, withHostBuildInfo, withSessionStatusFlag, type AutomationState, type AutomationRunState, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type Customization, type ISessionWithDefaultChat, type Message, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, IHostBuildInfo, SessionStatus } from '../common/state/sessionState.js'; import { AgentHostTelemetryLevelConfigKey, IPermissionsValue, platformRootSchema, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js'; import { SessionConfigKey } from '../common/sessionConfigKeys.js'; import { parseChangesetUri } from '../common/changesetUri.js'; @@ -257,7 +257,7 @@ export class AgentHostStateManager extends Disposable { * client-dispatchable and lazily create their state on first write. */ private readonly _annotations = new Map(); - private _automationCatalog: AutomationCatalogState | undefined; + private _automationCatalog: AutomationState | undefined; private readonly _automationRuns = new Map(); /** @@ -761,11 +761,11 @@ export class AgentHostStateManager extends Disposable { } /** Installs the durable automation catalogue before accepting subscriptions. */ - setAutomationCatalogState(state: AutomationCatalogState): void { + setAutomationCatalogState(state: AutomationState): void { this._automationCatalog = state; } - getAutomationCatalogState(): AutomationCatalogState | undefined { + getAutomationCatalogState(): AutomationState | undefined { return this._automationCatalog; } diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 8a3206b03583c..0ffe5c601a06c 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -37,7 +37,7 @@ import type { InvokeChangesetOperationParams, InvokeChangesetOperationResult } f import { AhpErrorCodes, AHP_SESSION_NOT_FOUND, ContentEncoding, JSON_RPC_INTERNAL_ERROR, ProtocolError, ResourceChangeType, ResourceType, ResourceWriteMode, type CreateResourceWatchParams, type CreateResourceWatchResult, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWatchState, type ResourceWriteParams, type ResourceWriteResult, type IStateSnapshot } from '../common/state/sessionProtocol.js'; import { ChangesSummary, ChatInteractivity, ChatOriginKind, MessageAttachmentKind, type Annotation, type AnnotationEntry, type AnnotationOrigin, type AnnotationsState, type ChatOrigin, type Customization, type Message, type MessageAttachment, type MessageResourceAttachment, type TextRange } from '../common/state/protocol/state.js'; import type { ChatPendingMessageSetAction, ChatTurnStartedAction, SessionConfigChangedAction } from '../common/state/protocol/actions.js'; -import { AUTOMATION_CATALOG_URI, isAhpAutomationRunChannel, ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_CREATED_BY_SESSION_DB_KEY, readSessionCreationReference, readSessionSpawnDepth, withSessionSpawnDepth, withSessionCreationReference, parseSessionCreationReference, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, TurnState, AH_META_WORKSPACELESS_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, getErrorResponsePart, isAhpChatChannel, isChatReadOnly, isDefaultChatUri, isSubagentChatUri, isSubagentSession, needsSessionGitStateRefresh, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withMessageHiddenFromTranscript, withSessionExternal, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionEhcliAdopted, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, readSessionEhcliAdoptable, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js'; +import { isAhpAutomationCatalogChannel, isAhpAutomationRunChannel, ISessionGitHubState, ISessionGitState, MessageKind, ResponsePartKind, SESSION_META_GITHUB_KEY, SESSION_META_GIT_KEY, SESSION_META_MULTI_ROOT_KEY, SESSION_META_SOURCE_CONTROL_KEY, AH_META_CREATED_BY_SESSION_DB_KEY, readSessionCreationReference, readSessionSpawnDepth, withSessionSpawnDepth, withSessionCreationReference, parseSessionCreationReference, SessionLifecycle, SessionStatus, ToolCallStatus, ToolResultContentType, TurnState, AH_META_WORKSPACELESS_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, AH_META_IS_ARCHIVED_DB_KEY, AH_META_IS_DONE_DB_KEY, AH_META_IS_READ_DB_KEY, buildChatUri, buildDefaultChatUri, buildResourceWatchChannelUri, buildSubagentChatUri, buildSubagentSessionUriPrefix, getErrorResponsePart, isAhpChatChannel, isChatReadOnly, isDefaultChatUri, isSubagentChatUri, isSubagentSession, needsSessionGitStateRefresh, parseChatUri, parseDefaultChatUri, parseRequiredSessionUriFromChatUri, parseResourceWatchChannelUri, parseSessionMultiRootMetadata, parseSubagentSessionUri, readSessionExternal, readSessionGitHubState, readSessionGitState, readSessionMultiRootMetadata, readSessionSourceControlState, readSessionWorkspaceless, withMessageHiddenFromTranscript, withSessionExternal, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionStatusFlag, withSessionWorkspaceless, withSessionEhcliAdopted, withSessionFolderPickerDecision, readSessionFolderPickerDecision, parseSessionFolderPickerDecision, SESSION_META_FOLDER_PICKER_KEY, readSessionEhcliAdoptable, type ISessionSourceControlState, type SessionConfigState, type SessionSummary, type ToolResultSubagentContent, type Turn } from '../common/state/sessionState.js'; import { readToolCallMeta } from '../common/meta/agentToolCallMeta.js'; import { isHostSnapshotAttachment, toHostSnapshotAttachmentMeta } from '../common/meta/agentSnapshotAttachmentMeta.js'; import { readEphemeralSessionMeta, withEphemeralSessionMeta } from '../common/meta/agentEphemeralSessionMeta.js'; @@ -4374,7 +4374,7 @@ export class AgentService extends Disposable implements IAgentService { } if (this._isAutomationAction(action)) { const origin = { clientId, clientSeq }; - if (channel !== AUTOMATION_CATALOG_URI) { + if (!isAhpAutomationCatalogChannel(channel)) { this._stateManager.rejectClientAction(channel, action, origin, 'Automation actions require the automation catalogue channel.'); return; } diff --git a/src/vs/platform/agentHost/node/protocolServerHandler.ts b/src/vs/platform/agentHost/node/protocolServerHandler.ts index 16d9b2a0a51d6..3bfa94f8ad6f0 100644 --- a/src/vs/platform/agentHost/node/protocolServerHandler.ts +++ b/src/vs/platform/agentHost/node/protocolServerHandler.ts @@ -49,7 +49,7 @@ import { type SubscribeResult, type ListSessionsResult, } from '../common/state/sessionProtocol.js'; -import { isAhpAutomationCatalogChannel, isAhpResourceWatchChannel, isAhpRootChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildDefaultChatUri, isAhpChatChannel, parseChatUri, parseRequiredSessionUriFromChatUri, type ISessionWithDefaultChat, type SessionState } from '../common/state/sessionState.js'; +import { isAhpAutomationCatalogChannel, isAhpResourceWatchChannel, ResponsePartKind, SessionStatus, ToolCallConfirmationReason, ToolCallContributorKind, ToolCallStatus, ToolResultContentType, buildDefaultChatUri, isAhpChatChannel, parseChatUri, parseRequiredSessionUriFromChatUri, type ISessionWithDefaultChat, type SessionState } from '../common/state/sessionState.js'; import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js'; import { IAgentHostManagedSettingsService } from './agentHostManagedSettingsService.js'; import { AgentHostStateManager } from './agentHostStateManager.js'; @@ -1993,9 +1993,6 @@ export class ProtocolServerHandler extends Disposable implements IAgentHostClien if ((sub?.kind === ChannelKind.State || sub?.kind === ChannelKind.ResourceWatch) && sub.active) { return true; } - if (!isAhpRootChannel(envelope.channel)) { - return false; - } return isActionEnvelopeRelevantToSubscriptionUris(envelope, this._stateAndResourceWatchUris(client)); } diff --git a/src/vs/platform/agentHost/test/common/agentSubscription.test.ts b/src/vs/platform/agentHost/test/common/agentSubscription.test.ts index c7b4b5b9fa4f8..706885b378e4d 100644 --- a/src/vs/platform/agentHost/test/common/agentSubscription.test.ts +++ b/src/vs/platform/agentHost/test/common/agentSubscription.test.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { buildAnnotationsUri } from '../../common/annotationsUri.js'; import { ActionType, type ActionEnvelope, type ClientChangesetAction } from '../../common/state/sessionActions.js'; -import { AutomationOperation, AutomationRunOriginKind, AutomationRunStatus, ChangesetStatus, MessageKind, ResponsePartKind, SessionLifecycle, SessionStatus, TerminalClaimKind, TerminalLifecycleStatus, TurnState, type AnnotationsState, type AutomationCatalogState, type AutomationRunState, type ChangesetState, type ErrorInfo, type RootState, type SessionState, type SessionSummary, type TerminalState, type Turn } from '../../common/state/protocol/state.js'; +import { AutomationOperation, AutomationRunOriginKind, AutomationRunStatus, ChangesetStatus, MessageKind, ResponsePartKind, SessionLifecycle, SessionStatus, TerminalClaimKind, TerminalLifecycleStatus, TurnState, type AnnotationsState, type AutomationRunState, type AutomationState, type ChangesetState, type ErrorInfo, type RootState, type SessionState, type SessionSummary, type TerminalState, type Turn } from '../../common/state/protocol/state.js'; import { AUTOMATION_CATALOG_URI, buildDefaultChatUri, createChatState, createDefaultChatSummary, getTurnError, ROOT_STATE_URI, StateComponents, type ChatState } from '../../common/state/sessionState.js'; import { AgentSubscriptionManager, AutomationCatalogSubscription, AutomationRunSubscription, ChangesetStateSubscription, ChatStateSubscription, isActionEnvelopeRelevantToSubscriptionUris, RootStateSubscription, SessionStateSubscription, TerminalStateSubscription } from '../../common/state/agentSubscription.js'; import { normalizeLegacyActionEnvelope, readLegacyTurnError } from '../../common/state/legacyProtocolCompatibility.js'; @@ -87,8 +87,8 @@ const changesetUri = `${sessionUri}/changeset/session`; const automationUri = 'ahp-automation:/test-automation'; const automationRunUri = 'ahp-automation-run:/test-run'; -function makeAutomationCatalogState(): AutomationCatalogState { - return { automations: [] }; +function makeAutomationCatalogState(): AutomationState { + return { entries: [] }; } function makeAutomationRunState(): AutomationRunState { @@ -121,7 +121,7 @@ suite('Automation subscriptions', () => { definition, }, 1, { clientId: 'c1', clientSeq: 1 }, undefined, AUTOMATION_CATALOG_URI)); - const requested = subscription.value as AutomationCatalogState; + const requested = subscription.value as AutomationState; subscription.receiveEnvelope(makeEnvelope({ type: ActionType.AutomationSet, automation: { @@ -135,8 +135,8 @@ suite('Automation subscriptions', () => { }, 2, undefined, undefined, AUTOMATION_CATALOG_URI)); assert.deepStrictEqual({ - requested: requested.automations, - authoritative: (subscription.value as AutomationCatalogState).automations.map(automation => automation.resource), + requested: requested.entries, + authoritative: (subscription.value as AutomationState).entries.map(automation => automation.resource), }, { requested: [], authoritative: [automationUri], @@ -178,7 +178,7 @@ suite('Automation subscriptions', () => { triggers: [], }; subscription.handleSnapshot({ - automations: [{ + entries: [{ resource: automationUri, definition, runs: [], @@ -194,7 +194,7 @@ suite('Automation subscriptions', () => { }, 1, { clientId: 'c1', clientSeq: 1 }, 'Automation has an active run.', AUTOMATION_CATALOG_URI)); assert.deepStrictEqual( - (subscription.value as AutomationCatalogState).automations.map(automation => automation.resource), + (subscription.value as AutomationState).entries.map(automation => automation.resource), [automationUri], ); }); @@ -852,7 +852,7 @@ suite('AgentSubscriptionManager', () => { ensureNoDisposablesAreLeakedInTestSuite(); - function createManager(subscribe: (resource: URI) => Promise<{ resource: string; state: SessionState | TerminalState | ChangesetState | AnnotationsState | AutomationCatalogState; fromSeq: number }> = async resource => { + function createManager(subscribe: (resource: URI) => Promise<{ resource: string; state: SessionState | TerminalState | ChangesetState | AnnotationsState | AutomationState; fromSeq: number }> = async resource => { const key = resource.toString(); subscribedResources.push(key); if (key.endsWith('/annotations')) { @@ -972,10 +972,15 @@ suite('AgentSubscriptionManager', () => { makeEnvelope({ type: ActionType.SessionTitleChanged, title: 'Yep' }, 3), ['ahp-root:', sessionUri], ), + automationVariant: isActionEnvelopeRelevantToSubscriptionUris( + makeEnvelope({ type: ActionType.AutomationRemoved, resource: automationUri }, 4, undefined, undefined, AUTOMATION_CATALOG_URI), + [URI.parse(AUTOMATION_CATALOG_URI).toString()], + ), }, { rootVariant: true, rootOnlyGetsSession: false, exactSession: true, + automationVariant: true, }); }); @@ -1003,12 +1008,13 @@ suite('AgentSubscriptionManager', () => { ref.dispose(); }); - test('uses the round-trippable automation catalogue URI', async () => { + test('uses the normalized automation catalogue URI internally', async () => { + const normalizedCatalogUri = URI.parse(AUTOMATION_CATALOG_URI).toString(); const mgr = createManager(async resource => { subscribedResources.push(resource.toString()); - return { resource: resource.toString(), state: { automations: [] }, fromSeq: 0 }; + return { resource: resource.toString(), state: { entries: [] }, fromSeq: 0 }; }); - const ref = mgr.getSubscription(StateComponents.AutomationCatalog, URI.parse(AUTOMATION_CATALOG_URI), 'AutomationHolder'); + const ref = mgr.getSubscription(StateComponents.AutomationCatalog, URI.parse(AUTOMATION_CATALOG_URI), 'AutomationHolder'); await Event.toPromise(ref.object.onDidChange); assert.deepStrictEqual({ @@ -1016,13 +1022,13 @@ suite('AgentSubscriptionManager', () => { resources: mgr.currentSubscriptionUris().map(resource => resource.toString()), activeResource: mgr.getActiveSubscriptions()[0].resource.toString(), }, { - subscribedResources: [AUTOMATION_CATALOG_URI], - resources: [AUTOMATION_CATALOG_URI], - activeResource: AUTOMATION_CATALOG_URI, + subscribedResources: [normalizedCatalogUri], + resources: [normalizedCatalogUri], + activeResource: normalizedCatalogUri, }); ref.dispose(); - assert.deepStrictEqual(unsubscribedResources, [AUTOMATION_CATALOG_URI]); + assert.deepStrictEqual(unsubscribedResources, [normalizedCatalogUri]); }); test('dispatchOptimistic applies to matching session subscription', async () => { @@ -1272,10 +1278,10 @@ suite('AgentSubscriptionManager', () => { test('markSubscriptionsMissing handles the Automation catalogue URI', async () => { const mgr = createManager(async resource => ({ resource: resource.toString(), - state: { automations: [] }, + state: { entries: [] }, fromSeq: 0, })); - const ref = mgr.getSubscription(StateComponents.AutomationCatalog, URI.parse(AUTOMATION_CATALOG_URI), 'test'); + const ref = mgr.getSubscription(StateComponents.AutomationCatalog, URI.parse(AUTOMATION_CATALOG_URI), 'test'); await Event.toPromise(ref.object.onDidChange); mgr.markSubscriptionsMissing([URI.parse(AUTOMATION_CATALOG_URI)]); @@ -1285,7 +1291,7 @@ suite('AgentSubscriptionManager', () => { resource: mgr.getActiveSubscriptions()[0].resource.toString(), }, { valueIsError: true, - resource: AUTOMATION_CATALOG_URI, + resource: URI.parse(AUTOMATION_CATALOG_URI).toString(), }); ref.dispose(); }); diff --git a/src/vs/platform/agentHost/test/electron-browser/agentHostProtocolClient.test.ts b/src/vs/platform/agentHost/test/electron-browser/agentHostProtocolClient.test.ts index 7146d0eca9b49..06fc392df17c8 100644 --- a/src/vs/platform/agentHost/test/electron-browser/agentHostProtocolClient.test.ts +++ b/src/vs/platform/agentHost/test/electron-browser/agentHostProtocolClient.test.ts @@ -1072,9 +1072,9 @@ suite('AgentHostProtocolClient', () => { clientInfo: params.clientInfo, _meta: params._meta, }, { - // Every negotiable version is offered so an older host can negotiate down, + // Every compatible version is offered so an older host can negotiate down, // newest first so a current host still picks it. - protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS], + protocolVersions: SUPPORTED_PROTOCOL_VERSIONS.filter(version => version !== '0.8.0'), clientId: 'renderer-client-id', clientInfo, _meta: { @@ -1085,6 +1085,7 @@ suite('AgentHostProtocolClient', () => { }, }); assert.strictEqual(params.protocolVersions[0], PROTOCOL_VERSION); + assert.ok(!params.protocolVersions.includes('0.8.0')); // Reply with a successful handshake so `connect()` resolves and the // test can finish cleanly. @@ -2670,7 +2671,7 @@ suite('AgentHostProtocolClient', () => { const initialSubscribe = await waitForRequest(transports[0], 'subscribe'); transports[0].fireMessage({ jsonrpc: '2.0', id: initialSubscribe.id, - result: { snapshot: { resource: AUTOMATION_CATALOG_URI, state: { automations: [] }, fromSeq: 5 } }, + result: { snapshot: { resource: AUTOMATION_CATALOG_URI, state: { entries: [] }, fromSeq: 5 } }, }); await flushMicrotasks(); @@ -2701,10 +2702,12 @@ suite('AgentHostProtocolClient', () => { await flushMicrotasks(); assert.deepStrictEqual({ - channel: (restoredSubscribe.params as { channel: string }).channel, + initialChannel: (initialSubscribe.params as { channel: string }).channel, + restoredChannel: (restoredSubscribe.params as { channel: string }).channel, valueIsError: catalogRef.object.value instanceof Error, }, { - channel: AUTOMATION_CATALOG_URI, + initialChannel: URI.parse(AUTOMATION_CATALOG_URI).toString(), + restoredChannel: URI.parse(AUTOMATION_CATALOG_URI).toString(), valueIsError: true, }); diff --git a/src/vs/platform/agentHost/test/node/agentHostAutomationService.test.ts b/src/vs/platform/agentHost/test/node/agentHostAutomationService.test.ts index b7bad28219566..49033b78af4ce 100644 --- a/src/vs/platform/agentHost/test/node/agentHostAutomationService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostAutomationService.test.ts @@ -107,7 +107,7 @@ suite('AgentHostAutomationService', () => { }, { writeAttempts: 3, capabilities: { create: {}, schedules: {}, runCancellation: {}, runHistoryLimit: 50 }, - catalog: { automations: [], _meta: { [AGENT_HOST_AUTOMATION_CATALOG_MIGRATED_META_KEY]: true } }, + catalog: { entries: [], _meta: { [AGENT_HOST_AUTOMATION_CATALOG_MIGRATED_META_KEY]: true } }, }); }); @@ -131,6 +131,39 @@ suite('AgentHostAutomationService', () => { }); }); + test('version 1 automation storage maps automations to protocol entries', async () => { + const resource = 'ahp-automation:/review-changes'; + storageService.set('automations', { + version: 1, + catalog: { + automations: [{ + resource, + definition: definition(), + runs: [], + operations: [AutomationOperation.Update, AutomationOperation.Remove], + createdAt: '2026-01-01T00:00:00.000Z', + modifiedAt: '2026-01-01T00:00:00.000Z', + }], + }, + }); + await storageService.whenIdle(); + const service = createService(); + + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries.map(entry => entry.resource), [resource]); + + await service.completeMigration([resource]); + const stored = storageService.get<{ version: number; catalog: { entries?: unknown[]; automations?: unknown[] } }>('automations'); + assert.deepStrictEqual({ + version: stored?.version, + automationCount: stored?.catalog.automations?.length, + hasEntries: Object.hasOwn(stored?.catalog ?? {}, 'entries'), + }, { + version: 1, + automationCount: 1, + hasEntries: false, + }); + }); + test('failed catalogue persistence publishes nothing and a retry creates one entry', async () => { const service = createService(); await service.completeMigration(); @@ -138,13 +171,13 @@ suite('AgentHostAutomationService', () => { await assert.rejects(service.handleCreate(createAction()), /storage unavailable/); assert.deepStrictEqual(stateManager.getAutomationCatalogState(), { - automations: [], + entries: [], _meta: { [AGENT_HOST_AUTOMATION_CATALOG_MIGRATED_META_KEY]: true }, }); await service.handleCreate(createAction()); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations.map(automation => ({ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries.map(automation => ({ resource: automation.resource, operations: automation.operations, })), [{ @@ -162,21 +195,21 @@ suite('AgentHostAutomationService', () => { /1 expected automation resources are missing/, ); await assert.rejects(service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/review-changes', requestId: 'blocked-request', }), /migration must complete/); assert.deepStrictEqual({ capabilities: service.capabilities, - operations: stateManager.getAutomationCatalogState()?.automations[0].operations, + operations: stateManager.getAutomationCatalogState()?.entries[0].operations, }, { capabilities: { create: {}, schedules: {}, runCancellation: {}, runHistoryLimit: 50 }, operations: [AutomationOperation.Update, AutomationOperation.Remove], }); await service.completeMigration(['ahp-automation:/review-changes']); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations[0].operations, [ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries[0].operations, [ AutomationOperation.Update, AutomationOperation.Remove, AutomationOperation.Run, @@ -193,11 +226,11 @@ suite('AgentHostAutomationService', () => { await service.handleConfigurationChanged(); await assert.rejects(service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/review-changes', requestId: 'disabled-request', }), /Automations are disabled/); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations[0].operations, [ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries[0].operations, [ AutomationOperation.Update, AutomationOperation.Remove, ]); @@ -207,7 +240,7 @@ suite('AgentHostAutomationService', () => { config: { [AGENT_HOST_AUTOMATIONS_ENABLED_CONFIG_KEY]: true }, }); await service.handleConfigurationChanged(); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations[0].operations, [ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries[0].operations, [ AutomationOperation.Update, AutomationOperation.Remove, AutomationOperation.Run, @@ -247,7 +280,7 @@ suite('AgentHostAutomationService', () => { await enableAndCreate(service); const params = { - channel: 'ahp-automations://catalog' as const, + channel: 'ahp-automations://' as const, automation: 'ahp-automation:/review-changes', requestId: 'manual-request', }; @@ -265,7 +298,7 @@ suite('AgentHostAutomationService', () => { status: running?.lifecycle.status, sessions: running?.sessions, primarySession: running?.primarySession, - catalogRuns: stateManager.getAutomationCatalogState()?.automations[0].runs.length, + catalogRuns: stateManager.getAutomationCatalogState()?.entries[0].runs.length, startedMessageKind, }, { first: second, @@ -296,7 +329,7 @@ suite('AgentHostAutomationService', () => { assert.deepStrictEqual({ run: stateManager.getAutomationRunState(first.resource)?.lifecycle.status, - summary: stateManager.getAutomationCatalogState()?.automations[0].runs[0].lifecycle.status, + summary: stateManager.getAutomationCatalogState()?.entries[0].runs[0].lifecycle.status, }, { run: AutomationRunStatus.Completed, summary: AutomationRunStatus.Completed, @@ -315,14 +348,14 @@ suite('AgentHostAutomationService', () => { writeFailures = 1; await assert.rejects(service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/review-changes', requestId: 'failed-request', }), /storage unavailable/); assert.deepStrictEqual({ createCalls, - runs: stateManager.getAutomationCatalogState()?.automations[0].runs, + runs: stateManager.getAutomationCatalogState()?.entries[0].runs, }, { createCalls: 0, runs: [], @@ -355,7 +388,7 @@ suite('AgentHostAutomationService', () => { await enableAndCreate(service); const result = await service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/review-changes', requestId: 'deferred-request', }); @@ -412,7 +445,7 @@ suite('AgentHostAutomationService', () => { )); const result = await service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/review-changes', requestId: 'hung-request', }); @@ -423,7 +456,7 @@ suite('AgentHostAutomationService', () => { assert.deepStrictEqual({ status: run?.lifecycle.status, error: run?.lifecycle.status === AutomationRunStatus.Failed ? run.lifecycle.error.message : undefined, - removeAvailable: stateManager.getAutomationCatalogState()?.automations[0].operations.includes(AutomationOperation.Remove), + removeAvailable: stateManager.getAutomationCatalogState()?.entries[0].operations.includes(AutomationOperation.Remove), }, { status: AutomationRunStatus.Failed, error: 'Automation run timed out.', @@ -461,7 +494,7 @@ suite('AgentHostAutomationService', () => { }); await enableAndCreate(service); const result = await service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/review-changes', requestId: 'cancel-request', }); @@ -514,7 +547,7 @@ suite('AgentHostAutomationService', () => { }); await enableAndCreate(service); const result = await service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/review-changes', requestId: 'cancel-failure', }); @@ -584,7 +617,7 @@ suite('AgentHostAutomationService', () => { }); await started.p; - const automation = stateManager.getAutomationCatalogState()?.automations[0]; + const automation = stateManager.getAutomationCatalogState()?.entries[0]; const run = automation?.runs[0]; assert.deepStrictEqual({ origin: run?.origin, @@ -672,7 +705,7 @@ suite('AgentHostAutomationService', () => { }); await started.p; - const automation = stateManager.getAutomationCatalogState()?.automations[0]; + const automation = stateManager.getAutomationCatalogState()?.entries[0]; const cursors = automation?._meta?.['vscode.scheduleCursors'] as Record | undefined; assert.deepStrictEqual({ runsClaimed: automation?.runs.length, @@ -754,7 +787,7 @@ suite('AgentHostAutomationService', () => { }); await started.p; - const automation = stateManager.getAutomationCatalogState()?.automations[0]; + const automation = stateManager.getAutomationCatalogState()?.entries[0]; assert.deepStrictEqual({ runsClaimed: automation?.runs.length, claimedTriggerId: automation?.runs[0]?.origin.kind === AutomationRunOriginKind.Trigger ? automation.runs[0].origin.triggerId : undefined, @@ -806,22 +839,22 @@ suite('AgentHostAutomationService', () => { const service = createService(); assert.deepStrictEqual({ - count: stateManager.getAutomationCatalogState()?.automations[0].runs.length, - cursor: stateManager.getAutomationCatalogState()?.automations[0].runsNextCursor, + count: stateManager.getAutomationCatalogState()?.entries[0].runs.length, + cursor: stateManager.getAutomationCatalogState()?.entries[0].runsNextCursor, }, { count: 50, cursor: '50', }); await service.fetchAutomationRuns({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: automationResource, cursor: '50', }); assert.deepStrictEqual({ - count: stateManager.getAutomationCatalogState()?.automations[0].runs.length, - cursor: stateManager.getAutomationCatalogState()?.automations[0].runsNextCursor, + count: stateManager.getAutomationCatalogState()?.entries[0].runs.length, + cursor: stateManager.getAutomationCatalogState()?.entries[0].runsNextCursor, }, { count: 51, cursor: undefined, @@ -841,11 +874,11 @@ suite('AgentHostAutomationService', () => { }); await assert.rejects(service.runAutomation({ - channel: 'ahp-automations://catalog', + channel: 'ahp-automations://', automation: 'ahp-automation:/pending-import', requestId: 'pending-request', }), /not available/i); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations[0].operations, [ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries[0].operations, [ AutomationOperation.Update, ]); }); @@ -867,7 +900,7 @@ suite('AgentHostAutomationService', () => { changes: { _meta: {} }, }); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations[0].operations, [ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries[0].operations, [ AutomationOperation.Update, AutomationOperation.Remove, AutomationOperation.Run, @@ -888,7 +921,7 @@ suite('AgentHostAutomationService', () => { changes: { _meta: { [AGENT_HOST_LEGACY_AUTOMATION_IMPORT_PENDING_META_KEY]: true } }, }); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations[0].operations, [ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries[0].operations, [ AutomationOperation.Update, ]); }); @@ -911,7 +944,7 @@ suite('AgentHostAutomationService', () => { await service.completeMigration(); - const automations = stateManager.getAutomationCatalogState()?.automations ?? []; + const automations = stateManager.getAutomationCatalogState()?.entries ?? []; const byResource = new Map(automations.map(automation => [automation.resource, automation.operations])); assert.deepStrictEqual({ pending: byResource.get('ahp-automation:/pending-import'), @@ -944,7 +977,7 @@ suite('AgentHostAutomationService', () => { }); await service.handleConfigurationChanged(); - assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.automations[0].operations, [ + assert.deepStrictEqual(stateManager.getAutomationCatalogState()?.entries[0].operations, [ AutomationOperation.Update, ]); }); @@ -995,7 +1028,7 @@ suite('AgentHostAutomationService', () => { }); await service.completeMigration(); - const automation = stateManager.getAutomationCatalogState()?.automations[0]; + const automation = stateManager.getAutomationCatalogState()?.entries[0]; assert.deepStrictEqual({ createCalls, operations: automation?.operations, diff --git a/src/vs/platform/agentHost/test/node/agentHostStorageService.test.ts b/src/vs/platform/agentHost/test/node/agentHostStorageService.test.ts index 072b0ac75cab5..365a22314d874 100644 --- a/src/vs/platform/agentHost/test/node/agentHostStorageService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostStorageService.test.ts @@ -92,7 +92,7 @@ suite('AgentHostStorageService', () => { try { const service = disposables.add(new AgentHostStorageService(URI.file(path), new NullLogService())); - assert.throws(() => service.set('automations', { catalog: { automations: [] } }), /persisted data could not be loaded/); + assert.throws(() => service.set('automations', { catalog: { entries: [] } }), /persisted data could not be loaded/); await assert.rejects(service.whenIdle(), /persisted data could not be loaded/); assert.deepStrictEqual({ hasLoadError: service.loadError instanceof Error, diff --git a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts index 968ca55af8d09..0511df6e9c71e 100644 --- a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts +++ b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts @@ -607,18 +607,19 @@ suite('ProtocolServerHandler', () => { assert.strictEqual(result.snapshots[0].resource.toString(), sessionUri.toString()); }); - test('automation catalogue subscription and run command preserve canonical channels', async () => { - stateManager.setAutomationCatalogState({ automations: [] }); + test('automation catalogue accepts URI-equivalent channels', async () => { + const normalizedCatalogUri = URI.parse(AUTOMATION_CATALOG_URI).toString(); + stateManager.setAutomationCatalogState({ entries: [] }); agentService.automationCapabilities = { create: {}, schedules: {}, runCancellation: {} }; agentService.automationRunResult = { resource: 'ahp-automation-run:/run-1' }; const transport = connectClient('automation-client'); const responsePromise = waitForResponse(transport, 2); - transport.simulateMessage(request(2, 'subscribe', { channel: AUTOMATION_CATALOG_URI })); + transport.simulateMessage(request(2, 'subscribe', { channel: normalizedCatalogUri })); const subscription = await responsePromise; const runResponsePromise = waitForResponse(transport, 3); transport.simulateMessage(request(3, 'runAutomation', { - channel: AUTOMATION_CATALOG_URI, + channel: normalizedCatalogUri, automation: 'ahp-automation:/automation-1', requestId: 'request-1', })); @@ -631,21 +632,29 @@ suite('ProtocolServerHandler', () => { response: hasKey(response, { result: true }) ? response.result : undefined, }, { snapshot: { - resource: AUTOMATION_CATALOG_URI, - state: { automations: [] }, + resource: normalizedCatalogUri, + state: { entries: [] }, fromSeq: stateManager.serverSeq, }, requests: [{ - channel: AUTOMATION_CATALOG_URI, + channel: normalizedCatalogUri, automation: 'ahp-automation:/automation-1', requestId: 'request-1', }], response: { resource: 'ahp-automation-run:/run-1' }, }); + + transport.sent.length = 0; + stateManager.dispatchServerAction(AUTOMATION_CATALOG_URI, { + type: ActionType.AutomationRemoved, + resource: 'ahp-automation:/automation-1', + }); + const action = findNotifications(transport.sent, 'action')[0]?.params as ActionEnvelope | undefined; + assert.strictEqual(action?.channel, AUTOMATION_CATALOG_URI); }); test('automation catalogue subscription rejects an inactive client before adding it', async () => { - stateManager.setAutomationCatalogState({ automations: [] }); + stateManager.setAutomationCatalogState({ entries: [] }); let subscriberAdded = false; agentService.addSubscriber = () => subscriberAdded = true; const target = handler as unknown as { diff --git a/src/vs/sessions/contrib/providers/agentHost/AGENT_HOST_SESSIONS_PROVIDER.md b/src/vs/sessions/contrib/providers/agentHost/AGENT_HOST_SESSIONS_PROVIDER.md index 278572214d1eb..cea2b5805ee62 100644 --- a/src/vs/sessions/contrib/providers/agentHost/AGENT_HOST_SESSIONS_PROVIDER.md +++ b/src/vs/sessions/contrib/providers/agentHost/AGENT_HOST_SESSIONS_PROVIDER.md @@ -43,7 +43,7 @@ The contribution also registers the content and working-directory adapters neede The cross-provider ownership, routing, migration, persistence, and run-lifecycle contract is specified in [AUTOMATIONS.md](../../../AUTOMATIONS.md). -Within that contract, Agent Host providers expose the host's `ahp-automations://catalog` when negotiated capabilities include Automations. `AgentHostAutomationStore` projects AHP state and maps host session resources into the local or remote Sessions resource scheme. `ReconnectableAgentHostAutomationStore` owns connection and compatibility transitions. After durable activation, the Agent Host owns execution and scheduling; this provider owns only adaptation and connection-specific identity. +Within that contract, Agent Host providers expose the host's `ahp-automations://` channel when negotiated capabilities include Automations. `AgentHostAutomationStore` projects AHP state and maps host session resources into the local or remote Sessions resource scheme. `ReconnectableAgentHostAutomationStore` owns connection and compatibility transitions. After durable activation, the Agent Host owns execution and scheduling; this provider owns only adaptation and connection-specific identity. Imported prompts retain Automation provenance through `MessageKind.Automation`. The projection converts editor-qualified model identifiers to provider-native `ModelSelection.id` values at the AHP boundary while preserving the editor identity exposed to Sessions. The provider also mirrors `chat.automations.enabled` and `chat.automations.runTimeoutMinutes` into host configuration; disabling Automations removes new run authority without deleting definitions or terminating sessions already running. diff --git a/src/vs/sessions/contrib/providers/agentHost/browser/agentHostAutomationStore.ts b/src/vs/sessions/contrib/providers/agentHost/browser/agentHostAutomationStore.ts index 39644389e9d17..a62240a173730 100644 --- a/src/vs/sessions/contrib/providers/agentHost/browser/agentHostAutomationStore.ts +++ b/src/vs/sessions/contrib/providers/agentHost/browser/agentHostAutomationStore.ts @@ -17,8 +17,8 @@ import { isAgentHostAutomationCatalogMigrated, isAgentHostLegacyAutomationImport import { SessionConfigKey } from '../../../../../platform/agentHost/common/sessionConfigKeys.js'; import { type IAgentSubscription } from '../../../../../platform/agentHost/common/state/agentSubscription.js'; import { ActionType } from '../../../../../platform/agentHost/common/state/sessionActions.js'; -import { AutomationMisfirePolicy, AutomationOperation, AutomationRunOriginKind, AutomationRunStatus, AutomationTriggerKind, MessageKind, type AutomationCatalogState, type AutomationDefinition, type AutomationRunSummary, type AutomationState } from '../../../../../platform/agentHost/common/state/protocol/state.js'; -import { AUTOMATION_CATALOG_URI, ROOT_STATE_URI, StateComponents } from '../../../../../platform/agentHost/common/state/sessionState.js'; +import { AutomationMisfirePolicy, AutomationOperation, AutomationRunOriginKind, AutomationRunStatus, AutomationTriggerKind, MessageKind, type AutomationDefinition, type AutomationEntry, type AutomationRunSummary, type AutomationState } from '../../../../../platform/agentHost/common/state/protocol/state.js'; +import { AUTOMATION_CATALOG_URI, isAhpAutomationCatalogChannel, ROOT_STATE_URI, StateComponents } from '../../../../../platform/agentHost/common/state/sessionState.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; @@ -44,7 +44,7 @@ export type IAgentHostAutomationConnection = Pick>; + ): IReference>; }; interface ISerializedArchivedRun extends Omit { @@ -73,8 +73,8 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro readonly preservesImportedRunHistory = true; - private readonly _catalogReference: IReference>; - private readonly _catalog: IAgentSubscription; + private readonly _catalogReference: IReference>; + private readonly _catalog: IAgentSubscription; private readonly _catalogChanged; private readonly _ready = observableValue(this, false); private readonly _runsForCache = new Map>(); @@ -123,8 +123,8 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro const catalog = this._catalog.value; if (catalog && !(catalog instanceof Error) && (isAgentHostAutomationCatalogMigrated(catalog) - || catalog.automations.some(automation => automation.operations.includes(AutomationOperation.Run))) - && !catalog.automations.some(automation => isAgentHostLegacyAutomationImportPending(automation.definition)) + || catalog.entries.some(automation => automation.operations.includes(AutomationOperation.Run))) + && !catalog.entries.some(automation => isAgentHostLegacyAutomationImportPending(automation.definition)) && (!this._legacySource || this._legacySource.automations.read(reader).length === 0) && !this._migrationPromise && !this._ready.read(reader)) { @@ -156,15 +156,15 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro getAutomation(id: string): IAutomationDescriptor | undefined { return this._ready.get() - ? this._projectAutomation(this._findAutomationState(id)) - : this._legacySource?.getAutomation(id) ?? this._projectAutomation(this._findAutomationState(id)); + ? this._projectAutomation(this._findAutomationEntry(id)) + : this._legacySource?.getAutomation(id) ?? this._projectAutomation(this._findAutomationEntry(id)); } isSchedulingOwnedByHost(automationId: string): boolean { if (!this._ready.get()) { return false; } - const state = this._findAutomationState(automationId); + const state = this._findAutomationEntry(automationId); return state !== undefined && !isAgentHostLegacyAutomationImportPending(state.definition) && state.operations.includes(AutomationOperation.Run); @@ -248,12 +248,12 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro this._requireOperation(id, AutomationOperation.Remove); mutationGuard?.(); const resource = automationResource(id); - if (!this._findAutomationState(id)) { + if (!this._findAutomationEntry(id)) { return; } await this._dispatchAndWait( { type: ActionType.AutomationRemoved, resource }, - catalog => !catalog.automations.some(automation => automation.resource === resource), + catalog => !catalog.entries.some(automation => automation.resource === resource), ); this._runsForCache.delete(id); } @@ -264,7 +264,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro private async _importAutomationSnapshot(snapshot: IAutomation, importPending: boolean): Promise { assertTerminalRunHistory(snapshot.runs); - const existing = this._findAutomationState(snapshot.automation.id); + const existing = this._findAutomationEntry(snapshot.automation.id); if (existing) { const current = this._requireProjectedAutomation(existing); const expected = this._canonicalDescriptor(snapshot.automation, existing); @@ -294,7 +294,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro async upsertAutomationSnapshot(snapshot: IAutomation): Promise { assertTerminalRunHistory(snapshot.runs); - if (this._findAutomationState(snapshot.automation.id)) { + if (this._findAutomationEntry(snapshot.automation.id)) { await this._replaceDescriptor(snapshot.automation, true, true); } else { await this._createDescriptor(snapshot.automation, true, true); @@ -303,7 +303,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro } async removeAutomationSnapshotIfUnchanged(expected: IAutomation): Promise { - const current = this._findAutomationState(expected.automation.id); + const current = this._findAutomationEntry(expected.automation.id); if (!current) { return { kind: 'missing' }; } @@ -317,7 +317,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro } async acknowledgeAutomationSnapshotImported(snapshot: IAutomation): Promise { - const current = this._findAutomationState(snapshot.automation.id); + const current = this._findAutomationEntry(snapshot.automation.id); if (!current || !isAgentHostLegacyAutomationImportPending(current.definition)) { return; } @@ -341,10 +341,10 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro automation: automationResource(automationId), requestId: generateUuid(), }); - const catalog = await this._waitForCatalog(state => state.automations.some(automation => automation.runs.some(run => + const catalog = await this._waitForCatalog(state => state.entries.some(automation => automation.runs.some(run => run.resource === result.resource && (run.primarySession !== undefined || isTerminalRun(run)) ))); - const run = catalog.automations.flatMap(automation => automation.runs).find(candidate => candidate.resource === result.resource); + const run = catalog.entries.flatMap(automation => automation.runs).find(candidate => candidate.resource === result.resource); if (!run) { throw new Error(`Automation run did not appear in the authoritative catalogue: ${result.resource}`); } @@ -354,7 +354,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro run: projectedRun, externalDispatch: { sessionResource: projectedRun.sessionResource, - whenCompleted: this._waitForCatalog(state => state.automations.some(automation => automation.runs.some(candidate => + whenCompleted: this._waitForCatalog(state => state.entries.some(automation => automation.runs.some(candidate => candidate.resource === result.resource && isTerminalRun(candidate) )), undefined, null).then(() => undefined), ...(this._connection.initializeResult.get()?.automations?.runCancellation ? { @@ -576,7 +576,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro } private async _clearImportPending(automationId: string): Promise { - const current = this._findAutomationState(automationId); + const current = this._findAutomationEntry(automationId); if (!current || !isAgentHostLegacyAutomationImportPending(current.definition)) { return; } @@ -593,7 +593,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro return; } const failures: Error[] = []; - const pending = catalog.automations.filter(automation => isAgentHostLegacyAutomationImportPending(automation.definition)); + const pending = catalog.entries.filter(automation => isAgentHostLegacyAutomationImportPending(automation.definition)); for (const automation of pending) { if (this._store.isDisposed) { throw new CancellationError(); @@ -630,7 +630,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro if (!catalog || catalog instanceof Error) { return []; } - return catalog.automations + return catalog.entries .map(automation => this._projectAutomation(automation)) .filter((automation): automation is IAutomationDescriptor => automation !== undefined) .sort((first, second) => second.createdAt.localeCompare(first.createdAt)); @@ -642,7 +642,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro if (!catalog || catalog instanceof Error) { return []; } - return catalog.automations + return catalog.entries .flatMap(automation => automation.runs) .map(run => this._projectRun(run)) .sort((first, second) => second.startedAt.localeCompare(first.startedAt)); @@ -650,11 +650,11 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro // Projects one Agent Host Automation's run summaries into editor-facing runs. private _projectRunsFor(resource: string): IAutomationRun[] { - return this._findAutomationStateByResource(resource)?.runs.map(run => this._projectRun(run)) ?? []; + return this._findAutomationEntryByResource(resource)?.runs.map(run => this._projectRun(run)) ?? []; } // Projects Agent Host Automation state into the editor-facing Automation model. - private _projectAutomation(state: AutomationState | undefined): IAutomationDescriptor | undefined { + private _projectAutomation(state: AutomationEntry | undefined): IAutomationDescriptor | undefined { if (!state) { return undefined; } @@ -729,14 +729,14 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro }; } - private _findAutomationState(id: string): AutomationState | undefined { - return this._findAutomationStateByResource(automationResource(id)); + private _findAutomationEntry(id: string): AutomationEntry | undefined { + return this._findAutomationEntryByResource(automationResource(id)); } - private _findAutomationStateByResource(resource: string): AutomationState | undefined { + private _findAutomationEntryByResource(resource: string): AutomationEntry | undefined { const catalog = this._catalog.value; return catalog && !(catalog instanceof Error) - ? catalog.automations.find(automation => automation.resource === resource) + ? catalog.entries.find(automation => automation.resource === resource) : undefined; } @@ -752,7 +752,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro if (!this._ready.get() && this._legacySource?.getAutomation(id)) { return true; } - return this._findAutomationState(id)?.operations.includes(operation) === true; + return this._findAutomationEntry(id)?.operations.includes(operation) === true; } private _requireOperation(id: string, operation: AutomationOperation): void { @@ -761,7 +761,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro } } - private _requireProjectedAutomation(state: AutomationState): IAutomationDescriptor { + private _requireProjectedAutomation(state: AutomationEntry): IAutomationDescriptor { const automation = this._projectAutomation(state); if (!automation) { throw new Error(`Automation cannot be represented by the compatibility view: ${state.resource}`); @@ -769,12 +769,12 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro return automation; } - private async _createDescriptor(descriptor: IAutomationDescriptor, imported = false, importPending?: boolean): Promise { + private async _createDescriptor(descriptor: IAutomationDescriptor, imported = false, importPending?: boolean): Promise { const resource = automationResource(descriptor.id); const definition = this._definitionFromDescriptor(descriptor, undefined, imported, importPending); const state = await this._dispatchAndWait( { type: ActionType.AutomationCreateRequested, resource, definition }, - catalog => catalog.automations.some(automation => automation.resource === resource), + catalog => catalog.entries.some(automation => automation.resource === resource), ); if (!state) { throw new Error(`Automation create completed without authoritative state: ${resource}`); @@ -782,9 +782,9 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro return state; } - private async _replaceDescriptor(descriptor: IAutomationDescriptor, imported = false, importPending?: boolean): Promise { + private async _replaceDescriptor(descriptor: IAutomationDescriptor, imported = false, importPending?: boolean): Promise { const resource = automationResource(descriptor.id); - const current = this._findAutomationState(descriptor.id); + const current = this._findAutomationEntry(descriptor.id); if (!current) { throw new Error(`Automation does not exist: ${descriptor.id}`); } @@ -804,7 +804,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro }, }, catalog => { - const state = catalog.automations.find(automation => automation.resource === resource); + const state = catalog.entries.find(automation => automation.resource === resource); const projected = this._projectAutomation(state); if (projected === undefined || serializeAutomationEditableState(projected) !== serializeAutomationEditableState(expected)) { @@ -894,7 +894,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro return prefix && !modelId.startsWith(prefix) ? `${prefix}${modelId}` : modelId; } - private _canonicalDescriptor(descriptor: IAutomationDescriptor, state: AutomationState): IAutomationDescriptor { + private _canonicalDescriptor(descriptor: IAutomationDescriptor, state: AutomationEntry): IAutomationDescriptor { const definition = this._definitionFromDescriptor(descriptor, state.definition); return this._requireProjectedAutomation({ ...state, definition }); } @@ -924,21 +924,21 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro private async _dispatchAndWait( action: Parameters[1] & { readonly resource: string }, - predicate: (catalog: AutomationCatalogState) => boolean, - ): Promise { + predicate: (catalog: AutomationState) => boolean, + ): Promise { await this._waitForCatalog(() => true); const result = this._waitForCatalog(predicate, action); this._connection.dispatch(AUTOMATION_CATALOG_URI, action); const catalog = await result; - const state = catalog.automations.find(automation => automation.resource === action.resource); + const state = catalog.entries.find(automation => automation.resource === action.resource); return state; } private _waitForCatalog( - predicate: (catalog: AutomationCatalogState) => boolean, + predicate: (catalog: AutomationState) => boolean, action?: { readonly type: ActionType; readonly resource: string }, timeoutMs: number | null = MUTATION_TIMEOUT_MS, - ): Promise { + ): Promise { if (this._store.isDisposed) { return Promise.reject(new CancellationError()); } @@ -949,7 +949,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro if (current && predicate(current)) { return Promise.resolve(current); } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const store = new DisposableStore(); const waitId = ++this._pendingWaitIds; let settled = false; @@ -960,7 +960,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro reject(new CancellationError()); } })); - const finish = (result: AutomationCatalogState | Error) => { + const finish = (result: AutomationState | Error) => { if (settled) { return; } @@ -986,7 +986,7 @@ export class AgentHostAutomationStore extends Disposable implements ISessionsPro } if (action) { store.add(this._connection.onDidAction(envelope => { - if (envelope.channel === AUTOMATION_CATALOG_URI + if (isAhpAutomationCatalogChannel(envelope.channel) && envelope.rejectionReason && envelope.action.type === action.type && hasKey(envelope.action, { resource: true }) diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/agentHostAutomationStore.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/agentHostAutomationStore.test.ts index f7765b2500d15..b355573ab8cd5 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/agentHostAutomationStore.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/agentHostAutomationStore.test.ts @@ -17,7 +17,7 @@ import type { IAgentConnection } from '../../../../../../platform/agentHost/comm import { AGENT_HOST_AUTOMATION_MIGRATION_CONFIG_KEY, AGENT_HOST_LEGACY_AUTOMATION_IMPORT_META_KEY, AGENT_HOST_LEGACY_AUTOMATION_IMPORT_PENDING_META_KEY } from '../../../../../../platform/agentHost/common/automationMigration.js'; import type { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; import { ActionType, type ActionEnvelope } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; -import { AutomationOperation, AutomationRunOriginKind, AutomationRunStatus, AutomationTriggerKind, MessageKind, type AutomationCatalogState, type AutomationState, type RootState } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; +import { AutomationOperation, AutomationRunOriginKind, AutomationRunStatus, AutomationTriggerKind, MessageKind, type AutomationEntry, type AutomationState, type RootState } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { AUTOMATION_CATALOG_URI, ROOT_STATE_URI, StateComponents } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import type { InitializeResult } from '../../../../../../platform/agentHost/common/state/protocol/common/commands.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; @@ -37,9 +37,9 @@ class TestAutomationConnection { private readonly _onDidAction = new Emitter(); readonly onDidAction = this._onDidAction.event; - private readonly _onDidCatalogChange = new Emitter(); + private readonly _onDidCatalogChange = new Emitter(); private readonly _onDidRootChange = new Emitter(); - private _catalog: AutomationCatalogState = { automations: [] }; + private _catalog: AutomationState = { entries: [] }; private _root: RootState; private _serverSeq = 0; private _migrationComplete: boolean; @@ -86,7 +86,7 @@ class TestAutomationConnection { kind: StateComponents.AutomationCatalog, resource: URI, _owner: string, - ): IReference> { + ): IReference> { assert.strictEqual(kind, StateComponents.AutomationCatalog); this.subscribedChannel = resource.toString(); const connection = this; @@ -122,7 +122,7 @@ class TestAutomationConnection { createdAt: timestamp, modifiedAt: timestamp, }; - this._catalog = { automations: [...this._catalog.automations, automation] }; + this._catalog = { entries: [...this._catalog.entries, automation] }; this._onDidCatalogChange.fire(this._catalog); this._onDidAction.fire({ channel: AUTOMATION_CATALOG_URI, @@ -134,7 +134,7 @@ class TestAutomationConnection { if (this.updateError) { throw this.updateError; } - const current = this._catalog.automations.find(automation => automation.resource === action.resource); + const current = this._catalog.entries.find(automation => automation.resource === action.resource); if (!current) { throw new Error(`Missing Automation: ${action.resource}`); } @@ -151,7 +151,7 @@ class TestAutomationConnection { modifiedAt: new Date().toISOString(), }; this._catalog = { - automations: this._catalog.automations.map(candidate => candidate.resource === automation.resource ? automation : candidate), + entries: this._catalog.entries.map(candidate => candidate.resource === automation.resource ? automation : candidate), }; this._onDidCatalogChange.fire(this._catalog); this._onDidAction.fire({ @@ -163,14 +163,14 @@ class TestAutomationConnection { } else if (action.type === ActionType.AutomationRemoved) { this._catalog = { ...this._catalog, - automations: this._catalog.automations.filter(automation => automation.resource !== action.resource), + entries: this._catalog.entries.filter(automation => automation.resource !== action.resource), }; this._onDidCatalogChange.fire(this._catalog); } else if (action.type === ActionType.RootConfigChanged && action.config[AGENT_HOST_AUTOMATION_MIGRATION_CONFIG_KEY]) { this._migrationComplete = true; this._catalog = { ...this._catalog, - automations: this._catalog.automations.map(automation => ({ + entries: this._catalog.entries.map(automation => ({ ...automation, operations: automation.definition._meta?.[AGENT_HOST_LEGACY_AUTOMATION_IMPORT_PENDING_META_KEY] ? automation.operations.filter(op => op !== AutomationOperation.Run && op !== AutomationOperation.Remove) @@ -197,7 +197,7 @@ class TestAutomationConnection { } async runAutomation(params: { readonly automation: string }) { - const automation = this._catalog.automations.find(candidate => candidate.resource === params.automation); + const automation = this._catalog.entries.find(candidate => candidate.resource === params.automation); if (!automation) { throw new Error(`Missing Automation: ${params.automation}`); } @@ -216,30 +216,30 @@ class TestAutomationConnection { }; this._catalog = { ...this._catalog, - automations: this._catalog.automations.map(candidate => candidate.resource === updated.resource ? updated : candidate), + entries: this._catalog.entries.map(candidate => candidate.resource === updated.resource ? updated : candidate), }; this._onDidCatalogChange.fire(this._catalog); return { resource }; } setOperations(resource: string, operations: AutomationOperation[]): void { - const current = this._catalog.automations.find(automation => automation.resource === resource); + const current = this._catalog.entries.find(automation => automation.resource === resource); if (!current) { throw new Error(`Missing Automation: ${resource}`); } const automation = { ...current, operations }; this._catalog = { ...this._catalog, - automations: this._catalog.automations.map(candidate => candidate.resource === resource ? automation : candidate), + entries: this._catalog.entries.map(candidate => candidate.resource === resource ? automation : candidate), }; this._onDidCatalogChange.fire(this._catalog); } - setAutomation(automation: AutomationState): void { + setAutomation(automation: AutomationEntry): void { this._catalog = { ...this._catalog, - automations: [ - ...this._catalog.automations.filter(candidate => candidate.resource !== automation.resource), + entries: [ + ...this._catalog.entries.filter(candidate => candidate.resource !== automation.resource), automation, ], }; @@ -250,7 +250,7 @@ class TestAutomationConnection { const timestamp = new Date().toISOString(); this._catalog = { ...this._catalog, - automations: this._catalog.automations.map(automation => ({ + entries: this._catalog.entries.map(automation => ({ ...automation, runs: automation.runs.map(run => run.resource === resource ? { ...run, @@ -422,7 +422,7 @@ suite('AgentHostAutomationStore', () => { enabled: automation.enabled, }, }, { - subscribedChannel: AUTOMATION_CATALOG_URI, + subscribedChannel: URI.parse(AUTOMATION_CATALOG_URI).toString(), dispatchChannel: AUTOMATION_CATALOG_URI, definitionMeta: undefined, triggerExpression: '30 9 * * *', @@ -1626,7 +1626,7 @@ suite('AgentHostAutomationStore', () => { subscriptions: connection.subscribedChannel, completionRequests: connection.dispatched.filter(entry => entry.channel === ROOT_STATE_URI).length, }, { - subscriptions: AUTOMATION_CATALOG_URI, + subscriptions: URI.parse(AUTOMATION_CATALOG_URI).toString(), completionRequests: 1, }); }); diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts index 03cda88c65122..2609e1288555a 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/localAgentHostSessionsProvider.test.ts @@ -20,8 +20,8 @@ import { AgentSession, type IAgentCreateChatRequestOptions, type IAgentCreateSes import { AgentHostCodexAgentEnabledSettingId, IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; import type { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; import type { ResolveSessionConfigResult } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; -import { ChatInteractivity as ProtocolChatInteractivity, ChatOriginKind as ProtocolChatOriginKind, CustomizationEnablementKind, CustomizationLoadStatus, CustomizationType, McpServerStatus, MessageKind, SessionLifecycle, type AgentCustomization, type AgentInfo, type AutomationCatalogState, type ChangesSummary, type Customization, type RootState, type SessionActiveClient, type SessionConfigState, type SessionState, type SessionSummary } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; -import { AUTOMATION_CATALOG_URI, buildChatUri, buildDefaultChatUri, buildSubagentChatUri, ChangesetStatus, ResponsePartKind, SessionSourceControlOutcome, SessionStatus as ProtocolSessionStatus, StateComponents, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, withSessionCreationReference, withSessionEhcliAdoptable, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionWorkspaceless, type ChangesetState, type ChatState, type ChatSummary } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { ChatInteractivity as ProtocolChatInteractivity, ChatOriginKind as ProtocolChatOriginKind, CustomizationEnablementKind, CustomizationLoadStatus, CustomizationType, McpServerStatus, MessageKind, SessionLifecycle, type AgentCustomization, type AgentInfo, type AutomationState, type ChangesSummary, type Customization, type RootState, type SessionActiveClient, type SessionConfigState, type SessionState, type SessionSummary } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; +import { buildChatUri, buildDefaultChatUri, buildSubagentChatUri, ChangesetStatus, isAhpAutomationCatalogChannel, ResponsePartKind, SessionSourceControlOutcome, SessionStatus as ProtocolSessionStatus, StateComponents, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, withSessionCreationReference, withSessionEhcliAdoptable, withSessionGitHubState, withSessionGitState, withSessionMultiRootMetadata, withSessionSourceControlState, withSessionWorkspaceless, type ChangesetState, type ChatState, type ChatSummary } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { SessionArtifactType, withSessionArtifacts } from '../../../../../../platform/agentHost/common/sessionArtifacts.js'; import { ActionType, NotificationType, type ActionEnvelope, type IRootConfigChangedAction, type ChatAction, type SessionAction, type TerminalAction, type INotification, type ClientAnnotationsAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import { SessionConfigKey } from '../../../../../../platform/agentHost/common/sessionConfigKeys.js'; @@ -68,7 +68,7 @@ import { TestPathService } from '../../../../../../workbench/test/browser/workbe const STORAGE_KEY_REMEMBERED_SESSION_CONFIG_VALUES = 'sessions.agentHost.sessionConfigPicker.selectedValues'; -type SubscriptionState = SessionState | ChangesetState | ChatState | AutomationCatalogState; +type SubscriptionState = SessionState | ChangesetState | ChatState | AutomationState; class MockAgentHostService extends mock() { declare readonly _serviceBrand: undefined; @@ -259,8 +259,8 @@ class MockAgentHostService extends mock() { override getSubscription(_kind: StateComponents, resource: URI): IReference> { const key = resource.toString(); - if (key === AUTOMATION_CATALOG_URI && !this._sessionStateValues.has(key)) { - this._sessionStateValues.set(key, { automations: [] }); + if (isAhpAutomationCatalogChannel(key) && !this._sessionStateValues.has(key)) { + this._sessionStateValues.set(key, { entries: [] }); } return this._getSubscription(key); } diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts index 6bb3fea1bb358..2766b614aef4a 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts @@ -19,9 +19,9 @@ import { ChangesetKind } from '../../../../../../platform/agentHost/common/chang import { type IAgentConnection } from '../../../../../../platform/agentHost/common/agentService.js'; import { SessionArtifactType, withSessionArtifacts } from '../../../../../../platform/agentHost/common/sessionArtifacts.js'; import type { ResolveSessionConfigResult } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; -import { MessageKind, SessionLifecycle, type AgentInfo, type AutomationCatalogState, type RootState, type SessionConfigState, type SessionState } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; +import { MessageKind, SessionLifecycle, type AgentInfo, type AutomationState, type RootState, type SessionConfigState, type SessionState } from '../../../../../../platform/agentHost/common/state/protocol/state.js'; import { ActionType, NotificationType, type ActionEnvelope, type IRootConfigChangedAction, type SessionAction, type TerminalAction, type INotification, type ClientAnnotationsAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; -import { AUTOMATION_CATALOG_URI, buildDefaultChatUri, SessionStatus as ProtocolSessionStatus, StateComponents } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { buildDefaultChatUri, isAhpAutomationCatalogChannel, SessionStatus as ProtocolSessionStatus, StateComponents } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import type { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; @@ -133,9 +133,9 @@ class MockAgentConnection extends mock() { // ---- Session-state subscriptions --------------------------------------- - private readonly _sessionStateEmitters = new Map>(); + private readonly _sessionStateEmitters = new Map>(); private readonly _sessionStateErrorEmitters = new Map>(); - private readonly _sessionStateValues = new Map(); + private readonly _sessionStateValues = new Map(); public sessionSubscribeCounts = new Map(); public sessionUnsubscribeCounts = new Map(); /** @@ -146,8 +146,8 @@ class MockAgentConnection extends mock() { override getSubscription(_kind: StateComponents, resource: URI): IReference> { const key = resource.toString(); - if (key === AUTOMATION_CATALOG_URI && !this._sessionStateValues.has(key)) { - this._sessionStateValues.set(key, { automations: [] }); + if (isAhpAutomationCatalogChannel(key) && !this._sessionStateValues.has(key)) { + this._sessionStateValues.set(key, { entries: [] }); } return this._getSubscription(key); } @@ -156,7 +156,7 @@ class MockAgentConnection extends mock() { this.sessionSubscribeCounts.set(key, (this.sessionSubscribeCounts.get(key) ?? 0) + 1); let emitter = this._sessionStateEmitters.get(key); if (!emitter) { - emitter = new Emitter(); + emitter = new Emitter(); this._sessionStateEmitters.set(key, emitter); } let errorEmitter = this._sessionStateErrorEmitters.get(key);