From 60d6da96141f5dbca86536529780ca648166aedb Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 18 Mar 2026 13:09:11 -0700 Subject: [PATCH] agentHost: upstream reducer logic from AHP Goes with https://github.com/microsoft/agent-host-protocol/pull/11 --- scripts/sync-agent-host-protocol.ts | 101 +--- .../platform/agentHost/common/agentService.ts | 7 +- .../state/protocol/action-origin.generated.ts | 114 ++++ .../common/state/protocol/actions.ts | 113 ++-- .../common/state/protocol/commands.ts | 24 +- .../agentHost/common/state/protocol/errors.ts | 2 +- .../common/state/protocol/messages.ts | 2 +- .../common/state/protocol/notifications.ts | 15 +- .../common/state/protocol/reducers.ts | 491 ++++++++++++++++++ .../agentHost/common/state/protocol/state.ts | 149 +++--- .../common/state/protocol/version/registry.ts | 62 ++- .../agentHost/common/state/sessionActions.ts | 40 +- .../agentHost/common/state/sessionReducers.ts | 454 +--------------- .../agentHost/common/state/sessionState.ts | 30 +- .../agentHost/node/agentEventMapper.ts | 51 +- .../platform/agentHost/node/agentService.ts | 4 +- .../agentHost/node/agentSideEffects.ts | 18 +- .../agentHost/node/copilot/copilotAgent.ts | 9 +- .../agentHost/node/sessionStateManager.ts | 18 +- .../test/node/agentEventMapper.test.ts | 3 +- .../agentHost/test/node/agentService.test.ts | 8 +- .../test/node/agentSideEffects.test.ts | 32 +- .../platform/agentHost/test/node/mockAgent.ts | 5 +- .../test/node/protocolServerHandler.test.ts | 26 +- .../test/node/sessionStateManager.test.ts | 46 +- .../agentHost/agentHostSessionHandler.ts | 24 +- .../agentHost/stateToProgressAdapter.ts | 20 +- .../agentHostChatContribution.test.ts | 6 +- .../stateToProgressAdapter.test.ts | 38 +- 29 files changed, 974 insertions(+), 938 deletions(-) create mode 100644 src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts create mode 100644 src/vs/platform/agentHost/common/state/protocol/reducers.ts diff --git a/scripts/sync-agent-host-protocol.ts b/scripts/sync-agent-host-protocol.ts index 750e55686af..02469d17051 100644 --- a/scripts/sync-agent-host-protocol.ts +++ b/scripts/sync-agent-host-protocol.ts @@ -9,12 +9,10 @@ // npx tsx scripts/sync-agent-host-protocol.ts // // Transformations applied: -// 1. Converts `const enum` to `const` object + string literal union (VS Code -// tsconfig uses `preserveConstEnums` which makes `const enum` nominal). -// 2. Converts 2-space indentation to tabs. -// 3. Merges duplicate imports from the same module. -// 4. Formats with the project's tsfmt.json settings. -// 5. Adds Microsoft copyright header. +// 1. Converts 2-space indentation to tabs. +// 2. Merges duplicate imports from the same module. +// 3. Formats with the project's tsfmt.json settings. +// 4. Adds Microsoft copyright header. // // URI stays as `string` (the protocol's canonical representation). VS Code code // should call `URI.parse()` at point-of-use where a URI class is needed. @@ -70,6 +68,8 @@ const BANNER = '// allow-any-unicode-comment-file\n// DO NOT EDIT -- auto-genera const FILES: { src: string; dest: string }[] = [ { src: 'state.ts', dest: 'state.ts' }, { src: 'actions.ts', dest: 'actions.ts' }, + { src: 'action-origin.generated.ts', dest: 'action-origin.generated.ts' }, + { src: 'reducers.ts', dest: 'reducers.ts' }, { src: 'commands.ts', dest: 'commands.ts' }, { src: 'errors.ts', dest: 'errors.ts' }, { src: 'notifications.ts', dest: 'notifications.ts' }, @@ -168,99 +168,14 @@ function mergeDuplicateImports(content: string): string { }).join('\n'); } -// Global enum definitions collected from all files before per-file processing -let globalEnumDefs = new Map>(); -function collectAllEnumDefs(): void { - globalEnumDefs = new Map(); - for (const file of FILES) { - const srcPath = path.join(TYPES_DIR, file.src); - if (!fs.existsSync(srcPath)) { - continue; - } - const content = fs.readFileSync(srcPath, 'utf-8'); - content.replace( - /export const enum (\w+) \{([^}]+)\}/g, - (_match, name: string, body: string) => { - const members = new Map(); - for (const line of body.split('\n')) { - const memberMatch = line.match(/^\s*(\w+)\s*=\s*'([^']+)'/); - if (memberMatch) { - members.set(memberMatch[1], memberMatch[2]); - } - } - if (members.size > 0) { - globalEnumDefs.set(name, members); - } - return _match; - } - ); - } -} -/** - * Converts `const enum Foo { A = 'a', B = 'b' }` into: - * ``` - * export const Foo = { A: 'a', B: 'b' } as const; - * export type Foo = typeof Foo[keyof typeof Foo]; - * ``` - * Then replaces `Foo.A` in type positions with the string literal `'a'`, - * using the global enum definitions collected from all protocol files. - */ -function convertConstEnums(content: string): string { - // Replace the const enum declarations in this file - content = content.replace( - /export const enum (\w+) \{([^}]+)\}/g, - (_match, name: string) => { - const members = globalEnumDefs.get(name); - if (!members) { - return _match; - } - const objEntries = [...members.entries()].map(([k, v]) => ` ${k}: '${v}'`).join(',\n'); - return `export const ${name} = {\n${objEntries},\n} as const;\nexport type ${name} = typeof ${name}[keyof typeof ${name}];`; - } - ); - // Replace Enum.Member references with their resolved string literals - for (const [enumName, members] of globalEnumDefs) { - for (const [memberName, value] of members) { - const ref = `${enumName}.${memberName}`; - content = content.split(ref).join(`'${value}'`); - } - } - - // Remove value imports of enums that are no longer referenced as values - content = content.replace( - /import \{([^}]+)\} from '([^']+)';/g, - (_match, names: string, from: string) => { - const parts = names.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0); - const remaining = parts.filter((name: string) => { - if (!globalEnumDefs.has(name)) { - return true; - } - const uses = content.split(name).length - 1; - return uses > 1; - }); - if (remaining.length === 0) { - return ''; - } - if (remaining.length === parts.length) { - return _match; - } - return `import { ${remaining.join(', ')} } from '${from}';`; - } - ); - - return content; -} function processFile(src: string, dest: string, commitHash: string): void { let content = fs.readFileSync(src, 'utf-8'); content = stripExistingHeader(content); - // Convert `const enum` to plain `const` object + string literal union - content = convertConstEnums(content); - // Merge duplicate imports from the same module content = mergeDuplicateImports(content); @@ -297,10 +212,6 @@ function main() { console.log(` Dest: ${DEST_DIR}`); console.log(); - // Collect all enum definitions across all protocol files - collectAllEnumDefs(); - console.log(` Collected ${globalEnumDefs.size} const enums`); - // Copy protocol files for (const file of FILES) { const srcPath = path.join(TYPES_DIR, file.src); diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 9907d4e4da8..63c2c6a93e4 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -8,6 +8,7 @@ import { URI } from '../../../base/common/uri.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import type { IActionEnvelope, INotification, ISessionAction } from './state/sessionActions.js'; import type { IBrowseDirectoryResult, IStateSnapshot } from './state/sessionProtocol.js'; +import { AttachmentType, PermissionKind, type PolicyState } from './state/sessionState.js'; // IPC contract between the renderer and the agent host utility process. // Defines all serializable event types, the IAgent provider interface, @@ -52,7 +53,7 @@ export interface IAgentCreateSessionConfig { /** Serializable attachment passed alongside a message to the agent host. */ export interface IAgentAttachment { - readonly type: 'file' | 'directory' | 'selection'; + readonly type: AttachmentType; readonly path: string; readonly displayName?: string; /** For selections: the selected text. */ @@ -74,7 +75,7 @@ export interface IAgentModelInfo { readonly supportsReasoningEffort: boolean; readonly supportedReasoningEfforts?: readonly string[]; readonly defaultReasoningEffort?: string; - readonly policyState?: 'enabled' | 'disabled' | 'unconfigured'; + readonly policyState?: PolicyState; readonly billingMultiplier?: number; } @@ -190,7 +191,7 @@ export interface IAgentPermissionRequestEvent extends IAgentProgressEventBase { /** Unique ID for correlating the response. */ readonly requestId: string; /** The kind of permission being requested. */ - readonly permissionKind: 'shell' | 'write' | 'mcp' | 'read' | 'url'; + readonly permissionKind: PermissionKind; /** The tool call ID that triggered this permission request. */ readonly toolCallId?: string; /** File path involved (for read/write). */ diff --git a/src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts b/src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts new file mode 100644 index 00000000000..8dfc004dcbd --- /dev/null +++ b/src/vs/platform/agentHost/common/state/protocol/action-origin.generated.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// allow-any-unicode-comment-file +// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts +// Synced from agent-host-protocol @ 3116861 + +// Generated from types/actions.ts — do not edit +// Run `npm run generate` to regenerate. + +import { ActionType, type IStateAction, type IRootAgentsChangedAction, type IRootActiveSessionsChangedAction, type ISessionReadyAction, type ISessionCreationFailedAction, type ISessionTurnStartedAction, type ISessionDeltaAction, type ISessionResponsePartAction, type ISessionToolCallStartAction, type ISessionToolCallDeltaAction, type ISessionToolCallReadyAction, type ISessionToolCallConfirmedAction, type ISessionToolCallCompleteAction, type ISessionToolCallResultConfirmedAction, type ISessionPermissionRequestAction, type ISessionPermissionResolvedAction, type ISessionTurnCompleteAction, type ISessionTurnCancelledAction, type ISessionErrorAction, type ISessionTitleChangedAction, type ISessionUsageAction, type ISessionReasoningAction, type ISessionModelChangedAction, type ISessionServerToolsChangedAction, type ISessionActiveClientChangedAction, type ISessionActiveClientToolsChangedAction } from './actions.js'; + + +// ─── Root vs Session Action Unions ─────────────────────────────────────────── + +/** Union of all root-scoped actions. */ +export type IRootAction = + | IRootAgentsChangedAction + | IRootActiveSessionsChangedAction + ; + +/** Union of all session-scoped actions. */ +export type ISessionAction = + | ISessionReadyAction + | ISessionCreationFailedAction + | ISessionTurnStartedAction + | ISessionDeltaAction + | ISessionResponsePartAction + | ISessionToolCallStartAction + | ISessionToolCallDeltaAction + | ISessionToolCallReadyAction + | ISessionToolCallConfirmedAction + | ISessionToolCallCompleteAction + | ISessionToolCallResultConfirmedAction + | ISessionPermissionRequestAction + | ISessionPermissionResolvedAction + | ISessionTurnCompleteAction + | ISessionTurnCancelledAction + | ISessionErrorAction + | ISessionTitleChangedAction + | ISessionUsageAction + | ISessionReasoningAction + | ISessionModelChangedAction + | ISessionServerToolsChangedAction + | ISessionActiveClientChangedAction + | ISessionActiveClientToolsChangedAction + ; + +/** Union of session actions that clients may dispatch. */ +export type IClientSessionAction = + | ISessionTurnStartedAction + | ISessionToolCallConfirmedAction + | ISessionToolCallCompleteAction + | ISessionToolCallResultConfirmedAction + | ISessionPermissionResolvedAction + | ISessionTurnCancelledAction + | ISessionModelChangedAction + | ISessionActiveClientChangedAction + | ISessionActiveClientToolsChangedAction + ; + +/** Union of session actions that only the server may produce. */ +export type IServerSessionAction = + | ISessionReadyAction + | ISessionCreationFailedAction + | ISessionDeltaAction + | ISessionResponsePartAction + | ISessionToolCallStartAction + | ISessionToolCallDeltaAction + | ISessionToolCallReadyAction + | ISessionPermissionRequestAction + | ISessionTurnCompleteAction + | ISessionErrorAction + | ISessionTitleChangedAction + | ISessionUsageAction + | ISessionReasoningAction + | ISessionServerToolsChangedAction + ; + +// ─── Client-Dispatchable Map ───────────────────────────────────────────────── + +/** + * Exhaustive map indicating which action types may be dispatched by clients. + * Adding a new action to IStateAction without adding it here is a compile error. + */ +export const IS_CLIENT_DISPATCHABLE: { readonly [K in IStateAction['type']]: boolean } = { + [ActionType.RootAgentsChanged]: false, + [ActionType.RootActiveSessionsChanged]: false, + [ActionType.SessionReady]: false, + [ActionType.SessionCreationFailed]: false, + [ActionType.SessionTurnStarted]: true, + [ActionType.SessionDelta]: false, + [ActionType.SessionResponsePart]: false, + [ActionType.SessionToolCallStart]: false, + [ActionType.SessionToolCallDelta]: false, + [ActionType.SessionToolCallReady]: false, + [ActionType.SessionToolCallConfirmed]: true, + [ActionType.SessionToolCallComplete]: true, + [ActionType.SessionToolCallResultConfirmed]: true, + [ActionType.SessionPermissionRequest]: false, + [ActionType.SessionPermissionResolved]: true, + [ActionType.SessionTurnComplete]: false, + [ActionType.SessionTurnCancelled]: true, + [ActionType.SessionError]: false, + [ActionType.SessionTitleChanged]: false, + [ActionType.SessionUsage]: false, + [ActionType.SessionReasoning]: false, + [ActionType.SessionModelChanged]: true, + [ActionType.SessionServerToolsChanged]: false, + [ActionType.SessionActiveClientChanged]: true, + [ActionType.SessionActiveClientToolsChanged]: true, +}; diff --git a/src/vs/platform/agentHost/common/state/protocol/actions.ts b/src/vs/platform/agentHost/common/state/protocol/actions.ts index 157daf6b119..40f4b2734f4 100644 --- a/src/vs/platform/agentHost/common/state/protocol/actions.ts +++ b/src/vs/platform/agentHost/common/state/protocol/actions.ts @@ -5,9 +5,9 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -// Synced from agent-host-protocol @ a566419 +// Synced from agent-host-protocol @ 3116861 -import { ToolCallConfirmationReason, type URI, type StringOrMarkdown, type IAgentInfo, type IErrorInfo, type IUserMessage, type IResponsePart, type IToolCallResult, type IToolDefinition, type ISessionActiveClient, type IUsageInfo, type IPermissionRequest } from './state.js'; +import { ToolCallConfirmationReason, ToolCallCancellationReason, type URI, type StringOrMarkdown, type IAgentInfo, type IErrorInfo, type IUserMessage, type IResponsePart, type IToolCallResult, type IToolDefinition, type ISessionActiveClient, type IUsageInfo, type IPermissionRequest } from './state.js'; // ─── Action Type Enum ──────────────────────────────────────────────────────── @@ -17,34 +17,33 @@ import { ToolCallConfirmationReason, type URI, type StringOrMarkdown, type IAgen * * @category Actions */ -export const ActionType = { - RootAgentsChanged: 'root/agentsChanged', - RootActiveSessionsChanged: 'root/activeSessionsChanged', - SessionReady: 'session/ready', - SessionCreationFailed: 'session/creationFailed', - SessionTurnStarted: 'session/turnStarted', - SessionDelta: 'session/delta', - SessionResponsePart: 'session/responsePart', - SessionToolCallStart: 'session/toolCallStart', - SessionToolCallDelta: 'session/toolCallDelta', - SessionToolCallReady: 'session/toolCallReady', - SessionToolCallConfirmed: 'session/toolCallConfirmed', - SessionToolCallComplete: 'session/toolCallComplete', - SessionToolCallResultConfirmed: 'session/toolCallResultConfirmed', - SessionPermissionRequest: 'session/permissionRequest', - SessionPermissionResolved: 'session/permissionResolved', - SessionTurnComplete: 'session/turnComplete', - SessionTurnCancelled: 'session/turnCancelled', - SessionError: 'session/error', - SessionTitleChanged: 'session/titleChanged', - SessionUsage: 'session/usage', - SessionReasoning: 'session/reasoning', - SessionModelChanged: 'session/modelChanged', - SessionServerToolsChanged: 'session/serverToolsChanged', - SessionActiveClientChanged: 'session/activeClientChanged', - SessionActiveClientToolsChanged: 'session/activeClientToolsChanged', -} as const; -export type ActionType = typeof ActionType[keyof typeof ActionType]; +export const enum ActionType { + RootAgentsChanged = 'root/agentsChanged', + RootActiveSessionsChanged = 'root/activeSessionsChanged', + SessionReady = 'session/ready', + SessionCreationFailed = 'session/creationFailed', + SessionTurnStarted = 'session/turnStarted', + SessionDelta = 'session/delta', + SessionResponsePart = 'session/responsePart', + SessionToolCallStart = 'session/toolCallStart', + SessionToolCallDelta = 'session/toolCallDelta', + SessionToolCallReady = 'session/toolCallReady', + SessionToolCallConfirmed = 'session/toolCallConfirmed', + SessionToolCallComplete = 'session/toolCallComplete', + SessionToolCallResultConfirmed = 'session/toolCallResultConfirmed', + SessionPermissionRequest = 'session/permissionRequest', + SessionPermissionResolved = 'session/permissionResolved', + SessionTurnComplete = 'session/turnComplete', + SessionTurnCancelled = 'session/turnCancelled', + SessionError = 'session/error', + SessionTitleChanged = 'session/titleChanged', + SessionUsage = 'session/usage', + SessionReasoning = 'session/reasoning', + SessionModelChanged = 'session/modelChanged', + SessionServerToolsChanged = 'session/serverToolsChanged', + SessionActiveClientChanged = 'session/activeClientChanged', + SessionActiveClientToolsChanged = 'session/activeClientToolsChanged', +} // ─── Action Envelope ───────────────────────────────────────────────────────── @@ -99,7 +98,7 @@ interface IToolCallActionBase { * @version 1 */ export interface IRootAgentsChangedAction { - type: 'root/agentsChanged'; + type: ActionType.RootAgentsChanged; /** Updated agent list */ agents: IAgentInfo[]; } @@ -111,7 +110,7 @@ export interface IRootAgentsChangedAction { * @version 1 */ export interface IRootActiveSessionsChangedAction { - type: 'root/activeSessionsChanged'; + type: ActionType.RootActiveSessionsChanged; /** Current count of active sessions */ activeSessions: number; } @@ -125,7 +124,7 @@ export interface IRootActiveSessionsChangedAction { * @version 1 */ export interface ISessionReadyAction { - type: 'session/ready'; + type: ActionType.SessionReady; /** Session URI */ session: URI; } @@ -137,7 +136,7 @@ export interface ISessionReadyAction { * @version 1 */ export interface ISessionCreationFailedAction { - type: 'session/creationFailed'; + type: ActionType.SessionCreationFailed; /** Session URI */ session: URI; /** Error details */ @@ -152,7 +151,7 @@ export interface ISessionCreationFailedAction { * @clientDispatchable */ export interface ISessionTurnStartedAction { - type: 'session/turnStarted'; + type: ActionType.SessionTurnStarted; /** Session URI */ session: URI; /** Turn identifier */ @@ -168,7 +167,7 @@ export interface ISessionTurnStartedAction { * @version 1 */ export interface ISessionDeltaAction { - type: 'session/delta'; + type: ActionType.SessionDelta; /** Session URI */ session: URI; /** Turn identifier */ @@ -184,7 +183,7 @@ export interface ISessionDeltaAction { * @version 1 */ export interface ISessionResponsePartAction { - type: 'session/responsePart'; + type: ActionType.SessionResponsePart; /** Session URI */ session: URI; /** Turn identifier */ @@ -204,7 +203,7 @@ export interface ISessionResponsePartAction { * @version 1 */ export interface ISessionToolCallStartAction extends IToolCallActionBase { - type: 'session/toolCallStart'; + type: ActionType.SessionToolCallStart; /** Internal tool name (for debugging/logging) */ toolName: string; /** Human-readable tool name */ @@ -223,7 +222,7 @@ export interface ISessionToolCallStartAction extends IToolCallActionBase { * @version 1 */ export interface ISessionToolCallDeltaAction extends IToolCallActionBase { - type: 'session/toolCallDelta'; + type: ActionType.SessionToolCallDelta; /** Partial parameter content to append */ content: string; /** Updated progress message */ @@ -242,7 +241,7 @@ export interface ISessionToolCallDeltaAction extends IToolCallActionBase { * @version 1 */ export interface ISessionToolCallReadyAction extends IToolCallActionBase { - type: 'session/toolCallReady'; + type: ActionType.SessionToolCallReady; /** Message describing what the tool will do */ invocationMessage: StringOrMarkdown; /** Raw tool input */ @@ -259,7 +258,7 @@ export interface ISessionToolCallReadyAction extends IToolCallActionBase { * @clientDispatchable */ export interface ISessionToolCallApprovedAction extends IToolCallActionBase { - type: 'session/toolCallConfirmed'; + type: ActionType.SessionToolCallConfirmed; /** The tool call was approved */ approved: true; /** How the tool was confirmed */ @@ -277,11 +276,11 @@ export interface ISessionToolCallApprovedAction extends IToolCallActionBase { * @clientDispatchable */ export interface ISessionToolCallDeniedAction extends IToolCallActionBase { - type: 'session/toolCallConfirmed'; + type: ActionType.SessionToolCallConfirmed; /** The tool call was denied */ approved: false; /** Why the tool was cancelled */ - reason: 'denied' | 'skipped'; + reason: ToolCallCancellationReason.Denied | ToolCallCancellationReason.Skipped; /** What the user suggested doing instead */ userSuggestion?: IUserMessage; /** Optional explanation for the denial */ @@ -316,7 +315,7 @@ export type ISessionToolCallConfirmedAction = * @clientDispatchable */ export interface ISessionToolCallCompleteAction extends IToolCallActionBase { - type: 'session/toolCallComplete'; + type: ActionType.SessionToolCallComplete; /** Execution result */ result: IToolCallResult; /** If true, the result requires client approval before finalizing */ @@ -333,7 +332,7 @@ export interface ISessionToolCallCompleteAction extends IToolCallActionBase { * @clientDispatchable */ export interface ISessionToolCallResultConfirmedAction extends IToolCallActionBase { - type: 'session/toolCallResultConfirmed'; + type: ActionType.SessionToolCallResultConfirmed; /** Whether the result was approved */ approved: boolean; } @@ -345,7 +344,7 @@ export interface ISessionToolCallResultConfirmedAction extends IToolCallActionBa * @version 1 */ export interface ISessionPermissionRequestAction { - type: 'session/permissionRequest'; + type: ActionType.SessionPermissionRequest; /** Session URI */ session: URI; /** Turn identifier */ @@ -362,7 +361,7 @@ export interface ISessionPermissionRequestAction { * @clientDispatchable */ export interface ISessionPermissionResolvedAction { - type: 'session/permissionResolved'; + type: ActionType.SessionPermissionResolved; /** Session URI */ session: URI; /** Turn identifier */ @@ -380,7 +379,7 @@ export interface ISessionPermissionResolvedAction { * @version 1 */ export interface ISessionTurnCompleteAction { - type: 'session/turnComplete'; + type: ActionType.SessionTurnComplete; /** Session URI */ session: URI; /** Turn identifier */ @@ -395,7 +394,7 @@ export interface ISessionTurnCompleteAction { * @clientDispatchable */ export interface ISessionTurnCancelledAction { - type: 'session/turnCancelled'; + type: ActionType.SessionTurnCancelled; /** Session URI */ session: URI; /** Turn identifier */ @@ -409,7 +408,7 @@ export interface ISessionTurnCancelledAction { * @version 1 */ export interface ISessionErrorAction { - type: 'session/error'; + type: ActionType.SessionError; /** Session URI */ session: URI; /** Turn identifier */ @@ -425,7 +424,7 @@ export interface ISessionErrorAction { * @version 1 */ export interface ISessionTitleChangedAction { - type: 'session/titleChanged'; + type: ActionType.SessionTitleChanged; /** Session URI */ session: URI; /** New title */ @@ -439,7 +438,7 @@ export interface ISessionTitleChangedAction { * @version 1 */ export interface ISessionUsageAction { - type: 'session/usage'; + type: ActionType.SessionUsage; /** Session URI */ session: URI; /** Turn identifier */ @@ -455,7 +454,7 @@ export interface ISessionUsageAction { * @version 1 */ export interface ISessionReasoningAction { - type: 'session/reasoning'; + type: ActionType.SessionReasoning; /** Session URI */ session: URI; /** Turn identifier */ @@ -472,7 +471,7 @@ export interface ISessionReasoningAction { * @clientDispatchable */ export interface ISessionModelChangedAction { - type: 'session/modelChanged'; + type: ActionType.SessionModelChanged; /** Session URI */ session: URI; /** New model ID */ @@ -488,7 +487,7 @@ export interface ISessionModelChangedAction { * @version 1 */ export interface ISessionServerToolsChangedAction { - type: 'session/serverToolsChanged'; + type: ActionType.SessionServerToolsChanged; /** Session URI */ session: URI; /** Updated server tools list (full replacement) */ @@ -508,7 +507,7 @@ export interface ISessionServerToolsChangedAction { * @clientDispatchable */ export interface ISessionActiveClientChangedAction { - type: 'session/activeClientChanged'; + type: ActionType.SessionActiveClientChanged; /** Session URI */ session: URI; /** The new active client, or `null` to unset */ @@ -527,7 +526,7 @@ export interface ISessionActiveClientChangedAction { * @clientDispatchable */ export interface ISessionActiveClientToolsChangedAction { - type: 'session/activeClientToolsChanged'; + type: ActionType.SessionActiveClientToolsChanged; /** Session URI */ session: URI; /** Updated client tools list (full replacement) */ diff --git a/src/vs/platform/agentHost/common/state/protocol/commands.ts b/src/vs/platform/agentHost/common/state/protocol/commands.ts index d6c9c009a74..676841e0728 100644 --- a/src/vs/platform/agentHost/common/state/protocol/commands.ts +++ b/src/vs/platform/agentHost/common/state/protocol/commands.ts @@ -5,7 +5,7 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -// Synced from agent-host-protocol @ a566419 +// Synced from agent-host-protocol @ 3116861 import type { URI, ISnapshot, ISessionSummary, ITurn } from './state.js'; import type { IActionEnvelope, IStateAction } from './actions.js'; @@ -56,11 +56,10 @@ export interface IInitializeResult { * * @category Commands */ -export const ReconnectResultType = { - Replay: 'replay', - Snapshot: 'snapshot', -} as const; -export type ReconnectResultType = typeof ReconnectResultType[keyof typeof ReconnectResultType]; +export const enum ReconnectResultType { + Replay = 'replay', + Snapshot = 'snapshot', +} /** * Re-establishes a dropped connection. The server replays missed actions or @@ -89,7 +88,7 @@ export interface IReconnectParams { */ export interface IReconnectReplayResult { /** Discriminant */ - type: 'replay'; + type: ReconnectResultType.Replay; /** Missed action envelopes since `lastSeenServerSeq` */ actions: IActionEnvelope[]; } @@ -99,7 +98,7 @@ export interface IReconnectReplayResult { */ export interface IReconnectSnapshotResult { /** Discriminant */ - type: 'snapshot'; + type: ReconnectResultType.Snapshot; /** Fresh snapshots for each subscription */ snapshots: ISnapshot[]; } @@ -227,11 +226,10 @@ export interface IListSessionsResult { * * @category Commands */ -export const ContentEncoding = { - Base64: 'base64', - Utf8: 'utf-8', -} as const; -export type ContentEncoding = typeof ContentEncoding[keyof typeof ContentEncoding]; +export const enum ContentEncoding { + Base64 = 'base64', + Utf8 = 'utf-8', +} /** * Fetches large content referenced by a `ContentRef` in the state tree. diff --git a/src/vs/platform/agentHost/common/state/protocol/errors.ts b/src/vs/platform/agentHost/common/state/protocol/errors.ts index 071725afbf7..638189c2bc1 100644 --- a/src/vs/platform/agentHost/common/state/protocol/errors.ts +++ b/src/vs/platform/agentHost/common/state/protocol/errors.ts @@ -5,7 +5,7 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -// Synced from agent-host-protocol @ a566419 +// Synced from agent-host-protocol @ 3116861 // ─── Standard JSON-RPC Codes ───────────────────────────────────────────────── diff --git a/src/vs/platform/agentHost/common/state/protocol/messages.ts b/src/vs/platform/agentHost/common/state/protocol/messages.ts index 740d30c04a6..395da78f6ea 100644 --- a/src/vs/platform/agentHost/common/state/protocol/messages.ts +++ b/src/vs/platform/agentHost/common/state/protocol/messages.ts @@ -5,7 +5,7 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -// Synced from agent-host-protocol @ a566419 +// Synced from agent-host-protocol @ 3116861 import type { IInitializeParams, IInitializeResult, IReconnectParams, IReconnectResult, ISubscribeParams, ISubscribeResult, ICreateSessionParams, IDisposeSessionParams, IListSessionsParams, IListSessionsResult, IFetchContentParams, IFetchContentResult, IBrowseDirectoryParams, IBrowseDirectoryResult, IFetchTurnsParams, IFetchTurnsResult, IUnsubscribeParams, IDispatchActionParams } from './commands.js'; diff --git a/src/vs/platform/agentHost/common/state/protocol/notifications.ts b/src/vs/platform/agentHost/common/state/protocol/notifications.ts index 140858822eb..3a55ca3b658 100644 --- a/src/vs/platform/agentHost/common/state/protocol/notifications.ts +++ b/src/vs/platform/agentHost/common/state/protocol/notifications.ts @@ -5,7 +5,7 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -// Synced from agent-host-protocol @ a566419 +// Synced from agent-host-protocol @ 3116861 import type { URI, ISessionSummary } from './state.js'; @@ -16,11 +16,10 @@ import type { URI, ISessionSummary } from './state.js'; * * @category Protocol Notifications */ -export const NotificationType = { - SessionAdded: 'notify/sessionAdded', - SessionRemoved: 'notify/sessionRemoved', -} as const; -export type NotificationType = typeof NotificationType[keyof typeof NotificationType]; +export const enum NotificationType { + SessionAdded = 'notify/sessionAdded', + SessionRemoved = 'notify/sessionRemoved', +} /** * Broadcast to all connected clients when a new session is created. @@ -49,7 +48,7 @@ export type NotificationType = typeof NotificationType[keyof typeof Notification * ``` */ export interface ISessionAddedNotification { - type: 'notify/sessionAdded'; + type: NotificationType.SessionAdded; /** Summary of the new session */ summary: ISessionSummary; } @@ -74,7 +73,7 @@ export interface ISessionAddedNotification { * ``` */ export interface ISessionRemovedNotification { - type: 'notify/sessionRemoved'; + type: NotificationType.SessionRemoved; /** URI of the removed session */ session: URI; } diff --git a/src/vs/platform/agentHost/common/state/protocol/reducers.ts b/src/vs/platform/agentHost/common/state/protocol/reducers.ts new file mode 100644 index 00000000000..ce78d37dc40 --- /dev/null +++ b/src/vs/platform/agentHost/common/state/protocol/reducers.ts @@ -0,0 +1,491 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// allow-any-unicode-comment-file +// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts +// Synced from agent-host-protocol @ 3116861 + +import { ActionType } from './actions.js'; +import { SessionLifecycle, SessionStatus, TurnState, ToolCallStatus, ToolCallConfirmationReason, ToolCallCancellationReason, type IRootState, type ISessionState, type IToolCallState, type IToolCallCompletedState, type IToolCallCancelledState, type ITurn } from './state.js'; +import { IS_CLIENT_DISPATCHABLE, type IRootAction, type ISessionAction, type IClientSessionAction } from './action-origin.generated.js'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** + * Soft assertion for exhaustiveness checking. Place in the `default` branch of + * a switch on a discriminated union so the compiler errors when a new variant + * is added but not handled. + * + * At runtime, logs a warning instead of throwing so that forward-compatible + * clients receiving unknown actions from a newer server degrade gracefully. + */ +export function softAssertNever(value: never, log?: (msg: string) => void): void { + const msg = `Unhandled action type: ${(value as { type: string }).type}`; + (log ?? console.warn)(msg); +} + +/** Extracts the common base fields shared by all tool call lifecycle states. */ +function tcBase(tc: IToolCallState) { + return { + toolCallId: tc.toolCallId, + toolName: tc.toolName, + displayName: tc.displayName, + toolClientId: tc.toolClientId, + _meta: tc._meta, + }; +} + +/** + * Ends the active turn, finalizing it into a completed turn record. + */ +function endTurn( + state: ISessionState, + turnId: string, + turnState: TurnState, + summaryStatus: SessionStatus, + error?: { errorType: string; message: string; stack?: string }, +): ISessionState { + if (!state.activeTurn || state.activeTurn.id !== turnId) { + return state; + } + const active = state.activeTurn; + + const toolCalls: (IToolCallCompletedState | IToolCallCancelledState)[] = []; + for (const tc of Object.values(active.toolCalls)) { + if (tc.status === ToolCallStatus.Completed || tc.status === ToolCallStatus.Cancelled) { + toolCalls.push(tc); + } else { + // Force non-terminal tool calls into cancelled state. + toolCalls.push({ + status: ToolCallStatus.Cancelled, + ...tcBase(tc), + invocationMessage: tc.status === ToolCallStatus.Streaming ? (tc.invocationMessage ?? '') : tc.invocationMessage, + toolInput: tc.status === ToolCallStatus.Streaming ? undefined : tc.toolInput, + reason: ToolCallCancellationReason.Skipped, + }); + } + } + + const turn: ITurn = { + id: active.id, + userMessage: active.userMessage, + responseText: active.streamingText, + responseParts: active.responseParts, + toolCalls, + usage: active.usage, + state: turnState, + error, + }; + + return { + ...state, + turns: [...state.turns, turn], + activeTurn: undefined, + summary: { ...state.summary, status: summaryStatus, modifiedAt: Date.now() }, + }; +} + +/** + * Immutably updates a single tool call in the active turn's toolCalls map. + * Returns `state` unchanged if the active turn or tool call doesn't match. + */ +function updateToolCall( + state: ISessionState, + turnId: string, + toolCallId: string, + updater: (tc: IToolCallState) => IToolCallState, +): ISessionState { + const activeTurn = state.activeTurn; + if (!activeTurn || activeTurn.id !== turnId) { + return state; + } + + const existing = activeTurn.toolCalls[toolCallId]; + if (!existing) { + return state; + } + + return { + ...state, + activeTurn: { + ...activeTurn, + toolCalls: { + ...activeTurn.toolCalls, + [toolCallId]: updater(existing), + }, + }, + }; +} + +// ─── Root Reducer ──────────────────────────────────────────────────────────── + +/** + * Pure reducer for root state. Handles all {@link IRootAction} variants. + */ +export function rootReducer(state: IRootState, action: IRootAction, log?: (msg: string) => void): IRootState { + switch (action.type) { + case ActionType.RootAgentsChanged: + return { ...state, agents: action.agents }; + + case ActionType.RootActiveSessionsChanged: + return { ...state, activeSessions: action.activeSessions }; + + default: + softAssertNever(action, log); + return state; + } +} + +// ─── Session Reducer ───────────────────────────────────────────────────────── + +/** + * Pure reducer for session state. Handles all {@link ISessionAction} variants. + */ +export function sessionReducer(state: ISessionState, action: ISessionAction, log?: (msg: string) => void): ISessionState { + switch (action.type) { + // ── Lifecycle ────────────────────────────────────────────────────────── + + case ActionType.SessionReady: + return { + ...state, + lifecycle: SessionLifecycle.Ready, + summary: { ...state.summary, status: SessionStatus.Idle }, + }; + + case ActionType.SessionCreationFailed: + return { + ...state, + lifecycle: SessionLifecycle.CreationFailed, + creationError: action.error, + }; + + // ── Turn Lifecycle ──────────────────────────────────────────────────── + + case ActionType.SessionTurnStarted: + return { + ...state, + summary: { ...state.summary, status: SessionStatus.InProgress, modifiedAt: Date.now() }, + activeTurn: { + id: action.turnId, + userMessage: action.userMessage, + streamingText: '', + responseParts: [], + toolCalls: {}, + pendingPermissions: {}, + reasoning: '', + usage: undefined, + }, + }; + + case ActionType.SessionDelta: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { + ...state.activeTurn, + streamingText: state.activeTurn.streamingText + action.content, + }, + }; + + case ActionType.SessionResponsePart: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { + ...state.activeTurn, + responseParts: [...state.activeTurn.responseParts, action.part], + }, + }; + + case ActionType.SessionTurnComplete: + return endTurn(state, action.turnId, TurnState.Complete, SessionStatus.Idle); + + case ActionType.SessionTurnCancelled: + return endTurn(state, action.turnId, TurnState.Cancelled, SessionStatus.Idle); + + case ActionType.SessionError: + return endTurn(state, action.turnId, TurnState.Error, SessionStatus.Error, action.error); + + // ── Tool Call State Machine ─────────────────────────────────────────── + + case ActionType.SessionToolCallStart: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { + ...state.activeTurn, + toolCalls: { + ...state.activeTurn.toolCalls, + [action.toolCallId]: { + toolCallId: action.toolCallId, + toolName: action.toolName, + displayName: action.displayName, + toolClientId: action.toolClientId, + _meta: action._meta, + status: ToolCallStatus.Streaming, + }, + }, + }, + }; + + case ActionType.SessionToolCallDelta: + return updateToolCall(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.Streaming) { + return tc; + } + return { + ...tc, + partialInput: (tc.partialInput ?? '') + action.content, + invocationMessage: action.invocationMessage ?? tc.invocationMessage, + }; + }); + + case ActionType.SessionToolCallReady: + return updateToolCall(state, action.turnId, action.toolCallId, tc => { + const base = tcBase(tc); + if (action.confirmed) { + return { + status: ToolCallStatus.Running, + ...base, + invocationMessage: action.invocationMessage, + toolInput: action.toolInput, + confirmed: action.confirmed, + }; + } + return { + status: ToolCallStatus.PendingConfirmation, + ...base, + invocationMessage: action.invocationMessage, + toolInput: action.toolInput, + }; + }); + + case ActionType.SessionToolCallConfirmed: + return updateToolCall(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.PendingConfirmation) { + return tc; + } + const base = tcBase(tc); + if (action.approved) { + return { + status: ToolCallStatus.Running, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed: action.confirmed, + }; + } + return { + status: ToolCallStatus.Cancelled, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + reason: action.reason, + reasonMessage: action.reasonMessage, + userSuggestion: action.userSuggestion, + }; + }); + + case ActionType.SessionToolCallComplete: + return updateToolCall(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.Running && tc.status !== ToolCallStatus.PendingConfirmation) { + return tc; + } + const base = tcBase(tc); + const confirmed = tc.status === ToolCallStatus.Running + ? tc.confirmed + : ToolCallConfirmationReason.NotNeeded; + if (action.requiresResultConfirmation) { + return { + status: ToolCallStatus.PendingResultConfirmation, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed, + ...action.result, + }; + } + return { + status: ToolCallStatus.Completed, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed, + ...action.result, + }; + }); + + case ActionType.SessionToolCallResultConfirmed: + return updateToolCall(state, action.turnId, action.toolCallId, tc => { + if (tc.status !== ToolCallStatus.PendingResultConfirmation) { + return tc; + } + const base = tcBase(tc); + if (action.approved) { + return { + status: ToolCallStatus.Completed, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed: tc.confirmed, + success: tc.success, + pastTenseMessage: tc.pastTenseMessage, + content: tc.content, + structuredContent: tc.structuredContent, + error: tc.error, + }; + } + return { + status: ToolCallStatus.Cancelled, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + reason: ToolCallCancellationReason.ResultDenied, + }; + }); + + // ── Permissions ─────────────────────────────────────────────────────── + + case ActionType.SessionPermissionRequest: { + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + const pendingPermissions = { + ...state.activeTurn.pendingPermissions, + [action.request.requestId]: action.request, + }; + // If the permission is tied to a tool call, transition it to pending-confirmation + let toolCalls = state.activeTurn.toolCalls; + if (action.request.toolCallId) { + const tc = toolCalls[action.request.toolCallId]; + if (tc && (tc.status === ToolCallStatus.Running || tc.status === ToolCallStatus.Streaming)) { + toolCalls = { + ...toolCalls, + [action.request.toolCallId]: { + ...tc, + status: ToolCallStatus.PendingConfirmation, + invocationMessage: tc.invocationMessage ?? '', + }, + }; + } + } + return { + ...state, + activeTurn: { ...state.activeTurn, pendingPermissions, toolCalls }, + }; + } + + case ActionType.SessionPermissionResolved: { + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + const resolved = state.activeTurn.pendingPermissions[action.requestId]; + const { [action.requestId]: _, ...pendingPermissions } = state.activeTurn.pendingPermissions; + // If the permission was tied to a tool call, transition it based on approval + let toolCalls = state.activeTurn.toolCalls; + if (resolved?.toolCallId) { + const tc = toolCalls[resolved.toolCallId]; + if (tc && tc.status === ToolCallStatus.PendingConfirmation) { + const base = tcBase(tc); + const updated: IToolCallState = action.approved + ? { + status: ToolCallStatus.Running, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + confirmed: ToolCallConfirmationReason.UserAction, + } + : { + status: ToolCallStatus.Cancelled, + ...base, + invocationMessage: tc.invocationMessage, + toolInput: tc.toolInput, + reason: ToolCallCancellationReason.Denied, + }; + toolCalls = { ...toolCalls, [resolved.toolCallId]: updated }; + } + } + return { + ...state, + activeTurn: { ...state.activeTurn, pendingPermissions, toolCalls }, + }; + } + + // ── Metadata ────────────────────────────────────────────────────────── + + case ActionType.SessionTitleChanged: + return { + ...state, + summary: { ...state.summary, title: action.title, modifiedAt: Date.now() }, + }; + + case ActionType.SessionUsage: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { ...state.activeTurn, usage: action.usage }, + }; + + case ActionType.SessionReasoning: + if (!state.activeTurn || state.activeTurn.id !== action.turnId) { + return state; + } + return { + ...state, + activeTurn: { + ...state.activeTurn, + reasoning: state.activeTurn.reasoning + action.content, + }, + }; + + case ActionType.SessionModelChanged: + return { + ...state, + summary: { ...state.summary, model: action.model, modifiedAt: Date.now() }, + }; + + case ActionType.SessionServerToolsChanged: + return { ...state, serverTools: action.tools }; + + case ActionType.SessionActiveClientChanged: + return { + ...state, + activeClient: action.activeClient ?? undefined, + }; + + case ActionType.SessionActiveClientToolsChanged: + if (!state.activeClient) { + return state; + } + return { + ...state, + activeClient: { ...state.activeClient, tools: action.tools }, + }; + + default: + softAssertNever(action, log); + return state; + } +} + +// ─── Dispatch Validation ───────────────────────────────────────────────────── + +/** + * Type guard that checks whether an action may be dispatched by a client. + * + * Servers SHOULD call this to validate incoming `dispatchAction` requests + * and reject any action the client is not allowed to originate. + */ +export function isClientDispatchable(action: ISessionAction): action is IClientSessionAction { + return IS_CLIENT_DISPATCHABLE[action.type]; +} diff --git a/src/vs/platform/agentHost/common/state/protocol/state.ts b/src/vs/platform/agentHost/common/state/protocol/state.ts index 60e7672e77e..a037ca22059 100644 --- a/src/vs/platform/agentHost/common/state/protocol/state.ts +++ b/src/vs/platform/agentHost/common/state/protocol/state.ts @@ -5,7 +5,7 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -// Synced from agent-host-protocol @ a566419 +// Synced from agent-host-protocol @ 3116861 // ─── Type Aliases ──────────────────────────────────────────────────────────── @@ -27,12 +27,11 @@ export type StringOrMarkdown = string | { markdown: string }; * * @category Root State */ -export const PolicyState = { - Enabled: 'enabled', - Disabled: 'disabled', - Unconfigured: 'unconfigured', -} as const; -export type PolicyState = typeof PolicyState[keyof typeof PolicyState]; +export const enum PolicyState { + Enabled = 'enabled', + Disabled = 'disabled', + Unconfigured = 'unconfigured', +} /** * Global state shared with every client subscribed to `agenthost:/root`. @@ -85,24 +84,22 @@ export interface ISessionModelInfo { * * @category Session State */ -export const SessionLifecycle = { - Creating: 'creating', - Ready: 'ready', - CreationFailed: 'creationFailed', -} as const; -export type SessionLifecycle = typeof SessionLifecycle[keyof typeof SessionLifecycle]; +export const enum SessionLifecycle { + Creating = 'creating', + Ready = 'ready', + CreationFailed = 'creationFailed', +} /** * Current session status. * * @category Session State */ -export const SessionStatus = { - Idle: 'idle', - InProgress: 'in-progress', - Error: 'error', -} as const; -export type SessionStatus = typeof SessionStatus[keyof typeof SessionStatus]; +export const enum SessionStatus { + Idle = 'idle', + InProgress = 'in-progress', + Error = 'error', +} /** * Full state for a single session, loaded when a client subscribes to the session's URI. @@ -170,24 +167,22 @@ export interface ISessionSummary { * * @category Turn Types */ -export const TurnState = { - Complete: 'complete', - Cancelled: 'cancelled', - Error: 'error', -} as const; -export type TurnState = typeof TurnState[keyof typeof TurnState]; +export const enum TurnState { + Complete = 'complete', + Cancelled = 'cancelled', + Error = 'error', +} /** * Type of a message attachment. * * @category Turn Types */ -export const AttachmentType = { - File: 'file', - Directory: 'directory', - Selection: 'selection', -} as const; -export type AttachmentType = typeof AttachmentType[keyof typeof AttachmentType]; +export const enum AttachmentType { + File = 'file', + Directory = 'directory', + Selection = 'selection', +} /** * A completed request/response cycle. @@ -266,18 +261,17 @@ export interface IMessageAttachment { * * @category Response Parts */ -export const ResponsePartKind = { - Markdown: 'markdown', - ContentRef: 'contentRef', -} as const; -export type ResponsePartKind = typeof ResponsePartKind[keyof typeof ResponsePartKind]; +export const enum ResponsePartKind { + Markdown = 'markdown', + ContentRef = 'contentRef', +} /** * @category Response Parts */ export interface IMarkdownResponsePart { /** Discriminant */ - kind: 'markdown'; + kind: ResponsePartKind.Markdown; /** Markdown content */ content: string; } @@ -289,7 +283,7 @@ export interface IMarkdownResponsePart { */ export interface IContentRef { /** Discriminant */ - kind: 'contentRef'; + kind: ResponsePartKind.ContentRef; /** Content URI */ uri: string; /** Approximate size in bytes */ @@ -310,15 +304,14 @@ export type IResponsePart = IMarkdownResponsePart | IContentRef; * * @category Tool Call Types */ -export const ToolCallStatus = { - Streaming: 'streaming', - PendingConfirmation: 'pending-confirmation', - Running: 'running', - PendingResultConfirmation: 'pending-result-confirmation', - Completed: 'completed', - Cancelled: 'cancelled', -} as const; -export type ToolCallStatus = typeof ToolCallStatus[keyof typeof ToolCallStatus]; +export const enum ToolCallStatus { + Streaming = 'streaming', + PendingConfirmation = 'pending-confirmation', + Running = 'running', + PendingResultConfirmation = 'pending-result-confirmation', + Completed = 'completed', + Cancelled = 'cancelled', +} /** * How a tool call was confirmed for execution. @@ -329,24 +322,22 @@ export type ToolCallStatus = typeof ToolCallStatus[keyof typeof ToolCallStatus]; * * @category Tool Call Types */ -export const ToolCallConfirmationReason = { - NotNeeded: 'not-needed', - UserAction: 'user-action', - Setting: 'setting', -} as const; -export type ToolCallConfirmationReason = typeof ToolCallConfirmationReason[keyof typeof ToolCallConfirmationReason]; +export const enum ToolCallConfirmationReason { + NotNeeded = 'not-needed', + UserAction = 'user-action', + Setting = 'setting', +} /** * Why a tool call was cancelled. * * @category Tool Call Types */ -export const ToolCallCancellationReason = { - Denied: 'denied', - Skipped: 'skipped', - ResultDenied: 'result-denied', -} as const; -export type ToolCallCancellationReason = typeof ToolCallCancellationReason[keyof typeof ToolCallCancellationReason]; +export const enum ToolCallCancellationReason { + Denied = 'denied', + Skipped = 'skipped', + ResultDenied = 'result-denied', +} /** * Metadata common to all tool call states. @@ -428,7 +419,7 @@ export interface IToolCallResult { * @category Tool Call Types */ export interface IToolCallStreamingState extends IToolCallBase { - status: 'streaming'; + status: ToolCallStatus.Streaming; /** Partial parameters accumulated so far */ partialInput?: string; /** Progress message shown while parameters are streaming */ @@ -441,7 +432,7 @@ export interface IToolCallStreamingState extends IToolCallBase { * @category Tool Call Types */ export interface IToolCallPendingConfirmationState extends IToolCallBase, IToolCallParameterFields { - status: 'pending-confirmation'; + status: ToolCallStatus.PendingConfirmation; } /** @@ -450,7 +441,7 @@ export interface IToolCallPendingConfirmationState extends IToolCallBase, IToolC * @category Tool Call Types */ export interface IToolCallRunningState extends IToolCallBase, IToolCallParameterFields { - status: 'running'; + status: ToolCallStatus.Running; /** How the tool was confirmed for execution */ confirmed: ToolCallConfirmationReason; } @@ -461,7 +452,7 @@ export interface IToolCallRunningState extends IToolCallBase, IToolCallParameter * @category Tool Call Types */ export interface IToolCallPendingResultConfirmationState extends IToolCallBase, IToolCallParameterFields, IToolCallResult { - status: 'pending-result-confirmation'; + status: ToolCallStatus.PendingResultConfirmation; /** How the tool was confirmed for execution */ confirmed: ToolCallConfirmationReason; } @@ -472,7 +463,7 @@ export interface IToolCallPendingResultConfirmationState extends IToolCallBase, * @category Tool Call Types */ export interface IToolCallCompletedState extends IToolCallBase, IToolCallParameterFields, IToolCallResult { - status: 'completed'; + status: ToolCallStatus.Completed; /** How the tool was confirmed for execution */ confirmed: ToolCallConfirmationReason; } @@ -483,7 +474,7 @@ export interface IToolCallCompletedState extends IToolCallBase, IToolCallParamet * @category Tool Call Types */ export interface IToolCallCancelledState extends IToolCallBase, IToolCallParameterFields { - status: 'cancelled'; + status: ToolCallStatus.Cancelled; /** Why the tool was cancelled */ reason: ToolCallCancellationReason; /** Optional message explaining the cancellation */ @@ -584,11 +575,10 @@ export interface IToolAnnotations { * * @category Tool Result Content */ -export const ToolResultContentType = { - Text: 'text', - Binary: 'binary', -} as const; -export type ToolResultContentType = typeof ToolResultContentType[keyof typeof ToolResultContentType]; +export const enum ToolResultContentType { + Text = 'text', + Binary = 'binary', +} /** * Text content in a tool result. @@ -598,7 +588,7 @@ export type ToolResultContentType = typeof ToolResultContentType[keyof typeof To * @category Tool Result Content */ export interface IToolResultTextContent { - type: 'text'; + type: ToolResultContentType.Text; /** The text content */ text: string; } @@ -611,7 +601,7 @@ export interface IToolResultTextContent { * @category Tool Result Content */ export interface IToolResultBinaryContent { - type: 'binary'; + type: ToolResultContentType.Binary; /** Base64-encoded data */ data: string; /** Content type (e.g. `"image/png"`, `"application/pdf"`) */ @@ -638,14 +628,13 @@ export type IToolResultContent = * * @category Permission Types */ -export const PermissionKind = { - Shell: 'shell', - Write: 'write', - Mcp: 'mcp', - Read: 'read', - Url: 'url', -} as const; -export type PermissionKind = typeof PermissionKind[keyof typeof PermissionKind]; +export const enum PermissionKind { + Shell = 'shell', + Write = 'write', + Mcp = 'mcp', + Read = 'read', + Url = 'url', +} /** * @category Permission Types 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 e2d6d44e452..94193f19930 100644 --- a/src/vs/platform/agentHost/common/state/protocol/version/registry.ts +++ b/src/vs/platform/agentHost/common/state/protocol/version/registry.ts @@ -5,12 +5,10 @@ // allow-any-unicode-comment-file // DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts -// Synced from agent-host-protocol @ a566419 - -import type { IStateAction } from '../actions.js'; - -import type { IProtocolNotification } from '../notifications.js'; +// Synced from agent-host-protocol @ 3116861 +import { ActionType, type IStateAction } from '../actions.js'; +import { NotificationType, type IProtocolNotification } from '../notifications.js'; // ─── Protocol Version Constants ────────────────────────────────────────────── @@ -27,31 +25,31 @@ export const MIN_PROTOCOL_VERSION = 1; * Adding a new action to `IStateAction` without adding it here is a compile error. */ export const ACTION_INTRODUCED_IN: { readonly [K in IStateAction['type']]: number } = { - ['root/agentsChanged']: 1, - ['root/activeSessionsChanged']: 1, - ['session/ready']: 1, - ['session/creationFailed']: 1, - ['session/turnStarted']: 1, - ['session/delta']: 1, - ['session/responsePart']: 1, - ['session/toolCallStart']: 1, - ['session/toolCallDelta']: 1, - ['session/toolCallReady']: 1, - ['session/toolCallConfirmed']: 1, - ['session/toolCallComplete']: 1, - ['session/toolCallResultConfirmed']: 1, - ['session/permissionRequest']: 1, - ['session/permissionResolved']: 1, - ['session/turnComplete']: 1, - ['session/turnCancelled']: 1, - ['session/error']: 1, - ['session/titleChanged']: 1, - ['session/usage']: 1, - ['session/reasoning']: 1, - ['session/modelChanged']: 1, - ['session/serverToolsChanged']: 1, - ['session/activeClientChanged']: 1, - ['session/activeClientToolsChanged']: 1, + [ActionType.RootAgentsChanged]: 1, + [ActionType.RootActiveSessionsChanged]: 1, + [ActionType.SessionReady]: 1, + [ActionType.SessionCreationFailed]: 1, + [ActionType.SessionTurnStarted]: 1, + [ActionType.SessionDelta]: 1, + [ActionType.SessionResponsePart]: 1, + [ActionType.SessionToolCallStart]: 1, + [ActionType.SessionToolCallDelta]: 1, + [ActionType.SessionToolCallReady]: 1, + [ActionType.SessionToolCallConfirmed]: 1, + [ActionType.SessionToolCallComplete]: 1, + [ActionType.SessionToolCallResultConfirmed]: 1, + [ActionType.SessionPermissionRequest]: 1, + [ActionType.SessionPermissionResolved]: 1, + [ActionType.SessionTurnComplete]: 1, + [ActionType.SessionTurnCancelled]: 1, + [ActionType.SessionError]: 1, + [ActionType.SessionTitleChanged]: 1, + [ActionType.SessionUsage]: 1, + [ActionType.SessionReasoning]: 1, + [ActionType.SessionModelChanged]: 1, + [ActionType.SessionServerToolsChanged]: 1, + [ActionType.SessionActiveClientChanged]: 1, + [ActionType.SessionActiveClientToolsChanged]: 1, }; /** @@ -69,8 +67,8 @@ export function isActionKnownToVersion(action: IStateAction, clientVersion: numb * is a compile error. */ export const NOTIFICATION_INTRODUCED_IN: { readonly [K in IProtocolNotification['type']]: number } = { - ['notify/sessionAdded']: 1, - ['notify/sessionRemoved']: 1, + [NotificationType.SessionAdded]: 1, + [NotificationType.SessionRemoved]: 1, }; /** diff --git a/src/vs/platform/agentHost/common/state/sessionActions.ts b/src/vs/platform/agentHost/common/state/sessionActions.ts index ab2d5b3c085..e1242a2a995 100644 --- a/src/vs/platform/agentHost/common/state/sessionActions.ts +++ b/src/vs/platform/agentHost/common/state/sessionActions.ts @@ -57,14 +57,10 @@ export { // Consumers use these shorter names; they're type-only aliases. import type { - IActionEnvelope as _IActionEnvelope, IRootAgentsChangedAction, IRootActiveSessionsChangedAction, - ISessionCreationFailedAction, ISessionDeltaAction, - ISessionErrorAction, ISessionModelChangedAction, - ISessionReadyAction, ISessionReasoningAction, ISessionResponsePartAction, ISessionPermissionRequestAction, @@ -80,18 +76,20 @@ import type { ISessionTurnCompleteAction, ISessionTurnStartedAction, ISessionUsageAction, - ISessionServerToolsChangedAction, - ISessionActiveClientChangedAction, - ISessionActiveClientToolsChangedAction, IStateAction, } from './protocol/actions.js'; import type { IProtocolNotification } from './protocol/notifications.js'; +import type { IRootAction as IRootAction_, ISessionAction as ISessionAction_, IClientSessionAction as IClientSessionAction_, IServerSessionAction as IServerSessionAction_ } from './protocol/action-origin.generated.js'; + +export type IRootAction = IRootAction_; +export type ISessionAction = ISessionAction_; +export type IClientSessionAction = IClientSessionAction_; +export type IServerSessionAction = IServerSessionAction_; // Root actions export type IAgentsChangedAction = IRootAgentsChangedAction; export type IActiveSessionsChangedAction = IRootActiveSessionsChangedAction; -export type IRootAction = IAgentsChangedAction | IActiveSessionsChangedAction; // Session actions — short aliases export type ITurnStartedAction = ISessionTurnStartedAction; @@ -114,32 +112,6 @@ export type IUsageAction = ISessionUsageAction; export type IReasoningAction = ISessionReasoningAction; export type IModelChangedAction = ISessionModelChangedAction; -/** Union of all session-scoped actions. */ -export type ISessionAction = - | ISessionReadyAction - | ISessionCreationFailedAction - | ISessionTurnStartedAction - | ISessionDeltaAction - | ISessionResponsePartAction - | ISessionToolCallStartAction - | ISessionToolCallDeltaAction - | ISessionToolCallReadyAction - | ISessionToolCallConfirmedAction - | ISessionToolCallCompleteAction - | ISessionToolCallResultConfirmedAction - | ISessionPermissionRequestAction - | ISessionPermissionResolvedAction - | ISessionTurnCompleteAction - | ISessionTurnCancelledAction - | ISessionErrorAction - | ISessionTitleChangedAction - | ISessionUsageAction - | ISessionReasoningAction - | ISessionModelChangedAction - | ISessionServerToolsChangedAction - | ISessionActiveClientChangedAction - | ISessionActiveClientToolsChangedAction; - // Notifications export type INotification = IProtocolNotification; diff --git a/src/vs/platform/agentHost/common/state/sessionReducers.ts b/src/vs/platform/agentHost/common/state/sessionReducers.ts index d39c72ab58f..3b02a189e5d 100644 --- a/src/vs/platform/agentHost/common/state/sessionReducers.ts +++ b/src/vs/platform/agentHost/common/state/sessionReducers.ts @@ -3,457 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// Pure reducer functions for the sessions process protocol. -// See protocol.md -> Reducers for the full design. -// -// Both the server and clients run the same reducers. This is what makes -// write-ahead possible: the client can locally predict the result of its -// own action using the exact same logic the server will run. -// -// IMPORTANT: Reducers must be pure — no side effects, no I/O, no service -// calls. Server-side effects (e.g. forwarding to the Copilot SDK) are -// handled by a separate dispatch layer. +// Re-exports the protocol reducers and adds VS Code-specific helpers. +// The actual reducer logic lives in the auto-generated protocol layer. -import type { IRootAction, ISessionAction } from './sessionActions.js'; -import { - type ICompletedToolCall, - type IErrorInfo, - type IRootState, - type ISessionState, - type IToolCallState, - type ITurn, - createActiveTurn, - SessionLifecycle, - SessionStatus, - TurnState, -} from './sessionState.js'; +import type { IToolCallState, ICompletedToolCall } from './sessionState.js'; -// ---- Helper: extract common base fields from a tool call state -------------- - -function tcBase(tc: IToolCallState) { - return { - toolCallId: tc.toolCallId, - toolName: tc.toolName, - displayName: tc.displayName, - _meta: tc._meta, - }; -} - -// ---- Root reducer ----------------------------------------------------------- - -/** - * Reduces root-level actions into a new RootState. - * Root actions are server-only (clients observe but cannot produce them). - */ -export function rootReducer(state: IRootState, action: IRootAction): IRootState { - switch (action.type) { - case 'root/agentsChanged': { - return { ...state, agents: [...action.agents] }; - } - case 'root/activeSessionsChanged': { - return { ...state, activeSessions: action.activeSessions }; - } - } -} - -// ---- Session reducer -------------------------------------------------------- - -/** - * Reduces session-level actions into a new SessionState. - * Handles lifecycle, turn lifecycle, streaming deltas, tool calls, permissions. - */ -export function sessionReducer(state: ISessionState, action: ISessionAction): ISessionState { - switch (action.type) { - case 'session/ready': { - return { ...state, lifecycle: SessionLifecycle.Ready }; - } - case 'session/creationFailed': { - return { - ...state, - lifecycle: SessionLifecycle.CreationFailed, - creationError: action.error, - }; - } - case 'session/turnStarted': { - const activeTurn = createActiveTurn(action.turnId, action.userMessage); - return { - ...state, - activeTurn, - summary: { ...state.summary, status: SessionStatus.InProgress }, - }; - } - case 'session/delta': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - return { - ...state, - activeTurn: { - ...state.activeTurn, - streamingText: state.activeTurn.streamingText + action.content, - }, - }; - } - case 'session/responsePart': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - return { - ...state, - activeTurn: { - ...state.activeTurn, - responseParts: [...state.activeTurn.responseParts, action.part], - }, - }; - } - case 'session/toolCallStart': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - return { - ...state, - activeTurn: { - ...state.activeTurn, - toolCalls: { - ...state.activeTurn.toolCalls, - [action.toolCallId]: { - status: 'streaming', - toolCallId: action.toolCallId, - toolName: action.toolName, - displayName: action.displayName, - _meta: action._meta, - }, - }, - }, - }; - } - case 'session/toolCallDelta': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - const tc = state.activeTurn.toolCalls[action.toolCallId]; - if (!tc || tc.status !== 'streaming') { - return state; - } - return { - ...state, - activeTurn: { - ...state.activeTurn, - toolCalls: { - ...state.activeTurn.toolCalls, - [action.toolCallId]: { - ...tc, - partialInput: (tc.partialInput ?? '') + action.content, - invocationMessage: action.invocationMessage ?? tc.invocationMessage, - }, - }, - }, - }; - } - case 'session/toolCallReady': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - const tc = state.activeTurn.toolCalls[action.toolCallId]; - if (!tc) { - return state; - } - const base = tcBase(tc); - const updated: IToolCallState = action.confirmed - ? { - status: 'running', - ...base, - invocationMessage: action.invocationMessage, - toolInput: action.toolInput, - confirmed: action.confirmed, - } - : { - status: 'pending-confirmation', - ...base, - invocationMessage: action.invocationMessage, - toolInput: action.toolInput, - }; - return { - ...state, - activeTurn: { - ...state.activeTurn, - toolCalls: { ...state.activeTurn.toolCalls, [action.toolCallId]: updated }, - }, - }; - } - case 'session/toolCallConfirmed': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - const tc = state.activeTurn.toolCalls[action.toolCallId]; - if (!tc || tc.status !== 'pending-confirmation') { - return state; - } - const base = tcBase(tc); - const updated: IToolCallState = action.approved - ? { - status: 'running', - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - confirmed: action.confirmed, - } - : { - status: 'cancelled', - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - reason: action.reason, - reasonMessage: action.reasonMessage, - userSuggestion: action.userSuggestion, - }; - return { - ...state, - activeTurn: { - ...state.activeTurn, - toolCalls: { ...state.activeTurn.toolCalls, [action.toolCallId]: updated }, - }, - }; - } - case 'session/toolCallComplete': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - const tc = state.activeTurn.toolCalls[action.toolCallId]; - if (!tc || (tc.status !== 'running' && tc.status !== 'pending-confirmation')) { - return state; - } - const base = tcBase(tc); - const confirmed = tc.status === 'running' ? tc.confirmed : 'not-needed'; - const updated: IToolCallState = action.requiresResultConfirmation - ? { - status: 'pending-result-confirmation', - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - confirmed, - ...action.result, - } - : { - status: 'completed', - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - confirmed, - ...action.result, - }; - return { - ...state, - activeTurn: { - ...state.activeTurn, - toolCalls: { ...state.activeTurn.toolCalls, [action.toolCallId]: updated }, - }, - }; - } - case 'session/toolCallResultConfirmed': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - const tc = state.activeTurn.toolCalls[action.toolCallId]; - if (!tc || tc.status !== 'pending-result-confirmation') { - return state; - } - const base = tcBase(tc); - const updated: IToolCallState = action.approved - ? { - status: 'completed', - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - confirmed: tc.confirmed, - success: tc.success, - pastTenseMessage: tc.pastTenseMessage, - content: tc.content, - structuredContent: tc.structuredContent, - error: tc.error, - } - : { - status: 'cancelled', - ...base, - invocationMessage: tc.invocationMessage, - toolInput: tc.toolInput, - reason: 'result-denied', - }; - return { - ...state, - activeTurn: { - ...state.activeTurn, - toolCalls: { ...state.activeTurn.toolCalls, [action.toolCallId]: updated }, - }, - }; - } - case 'session/permissionRequest': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - const pendingPermissions = { ...state.activeTurn.pendingPermissions, [action.request.requestId]: action.request }; - let toolCalls = state.activeTurn.toolCalls; - if (action.request.toolCallId) { - const toolCall = toolCalls[action.request.toolCallId]; - if (toolCall && (toolCall.status === 'running' || toolCall.status === 'streaming')) { - toolCalls = { - ...toolCalls, - [action.request.toolCallId]: { - ...toolCall, - status: 'pending-confirmation', - invocationMessage: toolCall.invocationMessage ?? '', - }, - }; - } - } - return { - ...state, - activeTurn: { ...state.activeTurn, pendingPermissions, toolCalls }, - }; - } - case 'session/permissionResolved': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - const resolved = state.activeTurn.pendingPermissions[action.requestId]; - const { [action.requestId]: _, ...pendingPermissions } = state.activeTurn.pendingPermissions; - let toolCalls = state.activeTurn.toolCalls; - if (resolved?.toolCallId) { - const toolCall = toolCalls[resolved.toolCallId]; - if (toolCall && toolCall.status === 'pending-confirmation') { - const base = tcBase(toolCall); - const updated: IToolCallState = action.approved - ? { - status: 'running', - ...base, - invocationMessage: toolCall.invocationMessage, - toolInput: toolCall.toolInput, - confirmed: 'user-action', - } - : { - status: 'cancelled', - ...base, - invocationMessage: toolCall.invocationMessage, - toolInput: toolCall.toolInput, - reason: 'denied', - }; - toolCalls = { ...toolCalls, [resolved.toolCallId]: updated }; - } - } - return { - ...state, - activeTurn: { ...state.activeTurn, pendingPermissions, toolCalls }, - }; - } - case 'session/turnComplete': { - return finalizeTurn(state, action.turnId, TurnState.Complete); - } - case 'session/turnCancelled': { - return finalizeTurn(state, action.turnId, TurnState.Cancelled); - } - case 'session/error': { - return finalizeTurn(state, action.turnId, TurnState.Error, action.error); - } - case 'session/titleChanged': { - return { - ...state, - summary: { ...state.summary, title: action.title, modifiedAt: Date.now() }, - }; - } - case 'session/modelChanged': { - return { - ...state, - summary: { ...state.summary, model: action.model, modifiedAt: Date.now() }, - }; - } - case 'session/usage': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - return { - ...state, - activeTurn: { - ...state.activeTurn, - usage: action.usage, - }, - }; - } - case 'session/reasoning': { - if (!state.activeTurn || state.activeTurn.id !== action.turnId) { - return state; - } - return { - ...state, - activeTurn: { - ...state.activeTurn, - reasoning: state.activeTurn.reasoning + action.content, - }, - }; - } - case 'session/serverToolsChanged': { - return { ...state, serverTools: action.tools }; - } - case 'session/activeClientChanged': { - return { ...state, activeClient: action.activeClient ?? undefined }; - } - case 'session/activeClientToolsChanged': { - if (!state.activeClient) { - return state; - } - return { ...state, activeClient: { ...state.activeClient, tools: action.tools } }; - } - } -} - -// ---- Helpers ---------------------------------------------------------------- - -/** - * Moves the active turn into the completed turns array and clears `activeTurn`. - */ -function finalizeTurn(state: ISessionState, turnId: string, turnState: TurnState, error?: IErrorInfo): ISessionState { - if (!state.activeTurn || state.activeTurn.id !== turnId) { - return state; - } - const active = state.activeTurn; - - const completedToolCalls: ICompletedToolCall[] = []; - for (const tc of Object.values(active.toolCalls)) { - if (tc.status === 'completed') { - completedToolCalls.push(tc); - } else if (tc.status === 'cancelled') { - completedToolCalls.push(tc); - } else { - // For tool calls that are not in a terminal state when the turn - // finishes (e.g. still streaming or running), force them into - // a cancelled state so they are persisted properly. - completedToolCalls.push({ - status: 'cancelled', - ...tcBase(tc), - invocationMessage: tc.status === 'streaming' ? (tc.invocationMessage ?? '') : tc.invocationMessage, - toolInput: tc.status === 'streaming' ? undefined : tc.toolInput, - reason: 'skipped', - }); - } - } - - const finalizedTurn: ITurn = { - id: active.id, - userMessage: active.userMessage, - responseText: active.streamingText, - responseParts: active.responseParts, - toolCalls: completedToolCalls, - usage: active.usage, - state: turnState, - error, - }; - - return { - ...state, - turns: [...state.turns, finalizedTurn], - activeTurn: undefined, - summary: { ...state.summary, status: SessionStatus.Idle, modifiedAt: Date.now() }, - }; -} +// Re-export reducers from the protocol layer +export { rootReducer, sessionReducer, softAssertNever, isClientDispatchable } from './protocol/reducers.js'; // ---- Tool call metadata helpers (VS Code extensions via _meta) -------------- diff --git a/src/vs/platform/agentHost/common/state/sessionState.ts b/src/vs/platform/agentHost/common/state/sessionState.ts index 0120f77d1c0..f63a17bde4b 100644 --- a/src/vs/platform/agentHost/common/state/sessionState.ts +++ b/src/vs/platform/agentHost/common/state/sessionState.ts @@ -11,17 +11,19 @@ // helpers and re-exports. import { hasKey } from '../../../../base/common/types.js'; -import type { - IActiveTurn, - IRootState, - ISessionState, - ISessionSummary, - IToolCallCancelledState, - IToolCallCompletedState, - IToolCallResult, - IToolCallState, - IToolResultTextContent, - IUserMessage, +import { + SessionLifecycle, + ToolResultContentType, + type IActiveTurn, + type IRootState, + type ISessionState, + type ISessionSummary, + type IToolCallCancelledState, + type IToolCallCompletedState, + type IToolCallResult, + type IToolCallState, + type IToolResultTextContent, + type IUserMessage, } from './protocol/state.js'; // Re-export everything from the protocol state module @@ -58,7 +60,9 @@ export { type IUserMessage, type StringOrMarkdown, type URI, + AttachmentType, PolicyState, + PermissionKind, ResponsePartKind, SessionLifecycle, SessionStatus, @@ -100,7 +104,7 @@ export function getToolOutputText(result: IToolCallResult): string | undefined { } const textParts: IToolResultTextContent[] = []; for (const c of result.content) { - if (hasKey(c, { type: true }) && c.type === 'text') { + if (hasKey(c, { type: true }) && c.type === ToolResultContentType.Text) { textParts.push(c); } } @@ -122,7 +126,7 @@ export function createRootState(): IRootState { export function createSessionState(summary: ISessionSummary): ISessionState { return { summary, - lifecycle: 'creating', + lifecycle: SessionLifecycle.Creating, turns: [], activeTurn: undefined, }; diff --git a/src/vs/platform/agentHost/node/agentEventMapper.ts b/src/vs/platform/agentHost/node/agentEventMapper.ts index b886efeec0f..5f378e7b739 100644 --- a/src/vs/platform/agentHost/node/agentEventMapper.ts +++ b/src/vs/platform/agentHost/node/agentEventMapper.ts @@ -14,20 +14,21 @@ import type { IAgentDeltaEvent, IAgentTitleChangedEvent, } from '../common/agentService.js'; -import type { - ISessionAction, - IDeltaAction, - IToolCallStartAction, - IToolCallReadyAction, - IToolCallCompleteAction, - ITurnCompleteAction, - ISessionErrorAction, - IUsageAction, - ITitleChangedAction, - IPermissionRequestAction, - IReasoningAction, +import { + ActionType, + type ISessionAction, + type IDeltaAction, + type IToolCallStartAction, + type IToolCallReadyAction, + type IToolCallCompleteAction, + type ITurnCompleteAction, + type ISessionErrorAction, + type IUsageAction, + type ITitleChangedAction, + type IPermissionRequestAction, + type IReasoningAction, } from '../common/state/sessionActions.js'; -import type { URI } from '../common/state/sessionState.js'; +import { ToolCallConfirmationReason, ToolResultContentType, type URI } from '../common/state/sessionState.js'; /** * Maps a flat {@link IAgentProgressEvent} from the agent host into @@ -41,7 +42,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U switch (event.type) { case 'delta': return { - type: 'session/delta', + type: ActionType.SessionDelta, session, turnId, content: (event as IAgentDeltaEvent).content, @@ -53,7 +54,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U // (params complete → running with auto-confirm) as a pair. const e = event as IAgentToolStartEvent; const startAction: IToolCallStartAction = { - type: 'session/toolCallStart', + type: ActionType.SessionToolCallStart, session, turnId, toolCallId: e.toolCallId, @@ -62,13 +63,13 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U _meta: { toolKind: e.toolKind, language: e.language }, }; const readyAction: IToolCallReadyAction = { - type: 'session/toolCallReady', + type: ActionType.SessionToolCallReady, session, turnId, toolCallId: e.toolCallId, invocationMessage: e.invocationMessage, toolInput: e.toolInput, - confirmed: 'not-needed', + confirmed: ToolCallConfirmationReason.NotNeeded, }; return [startAction, readyAction]; } @@ -76,14 +77,14 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U case 'tool_complete': { const e = event as IAgentToolCompleteEvent; return { - type: 'session/toolCallComplete', + type: ActionType.SessionToolCallComplete, session, turnId, toolCallId: e.toolCallId, result: { success: e.success, pastTenseMessage: e.pastTenseMessage, - content: e.toolOutput !== undefined ? [{ type: 'text' as const, text: e.toolOutput }] : undefined, + content: e.toolOutput !== undefined ? [{ type: ToolResultContentType.Text, text: e.toolOutput }] : undefined, error: e.error, }, } satisfies IToolCallCompleteAction; @@ -91,7 +92,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U case 'idle': return { - type: 'session/turnComplete', + type: ActionType.SessionTurnComplete, session, turnId, } satisfies ITurnCompleteAction; @@ -99,7 +100,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U case 'error': { const e = event as IAgentErrorEvent; return { - type: 'session/error', + type: ActionType.SessionError, session, turnId, error: { @@ -113,7 +114,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U case 'usage': { const e = event as IAgentUsageEvent; return { - type: 'session/usage', + type: ActionType.SessionUsage, session, turnId, usage: { @@ -127,7 +128,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U case 'title_changed': return { - type: 'session/titleChanged', + type: ActionType.SessionTitleChanged, session, title: (event as IAgentTitleChangedEvent).title, } satisfies ITitleChangedAction; @@ -135,7 +136,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U case 'permission_request': { const e = event as IAgentPermissionRequestEvent; return { - type: 'session/permissionRequest', + type: ActionType.SessionPermissionRequest, session, turnId, request: { @@ -154,7 +155,7 @@ export function mapProgressEventToActions(event: IAgentProgressEvent, session: U case 'reasoning': return { - type: 'session/reasoning', + type: ActionType.SessionReasoning, session, turnId, content: (event as IAgentReasoningEvent).content, diff --git a/src/vs/platform/agentHost/node/agentService.ts b/src/vs/platform/agentHost/node/agentService.ts index 8c1fce8fdd3..ab8b67a7b72 100644 --- a/src/vs/platform/agentHost/node/agentService.ts +++ b/src/vs/platform/agentHost/node/agentService.ts @@ -10,7 +10,7 @@ import { URI } from '../../../base/common/uri.js'; import { ILogService } from '../../log/common/log.js'; import { IFileService } from '../../files/common/files.js'; import { AgentProvider, IAgentCreateSessionConfig, IAgent, IAgentService, IAgentSessionMetadata, AgentSession, IAgentDescriptor } from '../common/agentService.js'; -import type { IActionEnvelope, INotification, ISessionAction } from '../common/state/sessionActions.js'; +import { ActionType, type IActionEnvelope, type INotification, type ISessionAction } from '../common/state/sessionActions.js'; import type { IBrowseDirectoryResult, IStateSnapshot } from '../common/state/sessionProtocol.js'; import { SessionStatus, type ISessionSummary } from '../common/state/sessionState.js'; import { AgentSideEffects } from './agentSideEffects.js'; @@ -140,7 +140,7 @@ export class AgentService extends Disposable implements IAgentService { modifiedAt: Date.now(), }; this._stateManager.createSession(summary); - this._stateManager.dispatchServerAction({ type: 'session/ready', session: session.toString() }); + this._stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: session.toString() }); return session; } diff --git a/src/vs/platform/agentHost/node/agentSideEffects.ts b/src/vs/platform/agentHost/node/agentSideEffects.ts index 73adeba5a01..c3cd4f362e6 100644 --- a/src/vs/platform/agentHost/node/agentSideEffects.ts +++ b/src/vs/platform/agentHost/node/agentSideEffects.ts @@ -10,7 +10,7 @@ import * as os from 'os'; import { IFileService } from '../../files/common/files.js'; import { ILogService } from '../../log/common/log.js'; import { IAgent, IAgentAttachment } from '../common/agentService.js'; -import type { ISessionAction } from '../common/state/sessionActions.js'; +import { ActionType, type ISessionAction } from '../common/state/sessionActions.js'; import { IBrowseDirectoryResult, ICreateSessionParams, AHP_PROVIDER_NOT_FOUND, JSON_RPC_INTERNAL_ERROR, ProtocolError, IDirectoryEntry } from '../common/state/sessionProtocol.js'; import { type ISessionModelInfo, @@ -79,7 +79,7 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH } return { provider: d.provider, displayName: d.displayName, description: d.description, models }; })); - this._stateManager.dispatchServerAction({ type: 'root/agentsChanged', agents: infos }); + this._stateManager.dispatchServerAction({ type: ActionType.RootAgentsChanged, agents: infos }); } // ---- Agent registration ------------------------------------------------- @@ -119,11 +119,11 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH handleAction(action: ISessionAction): void { switch (action.type) { - case 'session/turnStarted': { + case ActionType.SessionTurnStarted: { const agent = this._options.getAgent(action.session); if (!agent) { this._stateManager.dispatchServerAction({ - type: 'session/error', + type: ActionType.SessionError, session: action.session, turnId: action.turnId, error: { errorType: 'noAgent', message: 'No agent found for session' }, @@ -138,7 +138,7 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH agent.sendMessage(URI.parse(action.session), action.userMessage.text, attachments).catch(err => { this._logService.error('[AgentSideEffects] sendMessage failed', err); this._stateManager.dispatchServerAction({ - type: 'session/error', + type: ActionType.SessionError, session: action.session, turnId: action.turnId, error: { errorType: 'sendFailed', message: String(err) }, @@ -146,7 +146,7 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH }); break; } - case 'session/permissionResolved': { + case ActionType.SessionPermissionResolved: { const providerId = this._pendingPermissions.get(action.requestId); if (providerId) { this._pendingPermissions.delete(action.requestId); @@ -157,14 +157,14 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH } break; } - case 'session/turnCancelled': { + case ActionType.SessionTurnCancelled: { const agent = this._options.getAgent(action.session); agent?.abortSession(URI.parse(action.session)).catch(err => { this._logService.error('[AgentSideEffects] abortSession failed', err); }); break; } - case 'session/modelChanged': { + case ActionType.SessionModelChanged: { const agent = this._options.getAgent(action.session); agent?.changeModel?.(URI.parse(action.session), action.model).catch(err => { this._logService.error('[AgentSideEffects] changeModel failed', err); @@ -200,7 +200,7 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH modifiedAt: Date.now(), }; this._stateManager.createSession(summary); - this._stateManager.dispatchServerAction({ type: 'session/ready', session }); + this._stateManager.dispatchServerAction({ type: ActionType.SessionReady, session }); } handleDisposeSession(session: ProtocolURI): void { diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index 6f8ef0cd5b9..334edbba1d4 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -14,6 +14,7 @@ import { rgPath } from '@vscode/ripgrep'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ILogService } from '../../../log/common/log.js'; import { IAgentCreateSessionConfig, IAgentModelInfo, IAgentProgressEvent, IAgentMessageEvent, IAgent, IAgentSessionMetadata, IAgentToolStartEvent, IAgentToolCompleteEvent, AgentSession, IAgentDescriptor, IAgentAttachment } from '../../common/agentService.js'; +import { PermissionKind, type PolicyState } from '../../common/state/sessionState.js'; import { getInvocationMessage, getPastTenseMessage, getShellLanguage, getToolDisplayName, getToolInputString, getToolKind, isHiddenTool } from './copilotToolDisplay.js'; import { CopilotSessionWrapper } from './copilotSessionWrapper.js'; @@ -164,7 +165,7 @@ export class CopilotAgent extends Disposable implements IAgent { supportsReasoningEffort: m.capabilities.supports.reasoningEffort, supportedReasoningEfforts: m.supportedReasoningEfforts, defaultReasoningEffort: m.defaultReasoningEffort, - policyState: m.policy?.state, + policyState: m.policy?.state as PolicyState | undefined, billingMultiplier: m.billing?.multiplier, })); this._logService.info(`[Copilot] Found ${result.length} models`); @@ -304,9 +305,9 @@ export class CopilotAgent extends Disposable implements IAgent { const deferred = new DeferredPromise(); this._pendingPermissions.set(requestId, { sessionId: invocation.sessionId, deferred }); - const permissionKind = (['shell', 'write', 'mcp', 'read', 'url'] as const).includes(request.kind as 'shell') - ? request.kind as 'shell' | 'write' | 'mcp' | 'read' | 'url' - : 'read'; // Treat unknown kinds as read (safest default) + const permissionKind = ([PermissionKind.Shell, PermissionKind.Write, PermissionKind.Mcp, PermissionKind.Read, PermissionKind.Url] as const).includes(request.kind as PermissionKind) + ? request.kind as PermissionKind + : PermissionKind.Read; // Treat unknown kinds as read (safest default) // Fire the event so the renderer can handle it this._onDidSessionProgress.fire({ diff --git a/src/vs/platform/agentHost/node/sessionStateManager.ts b/src/vs/platform/agentHost/node/sessionStateManager.ts index 02561fc0948..99e8382f431 100644 --- a/src/vs/platform/agentHost/node/sessionStateManager.ts +++ b/src/vs/platform/agentHost/node/sessionStateManager.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { ILogService } from '../../log/common/log.js'; -import { IActionEnvelope, IActionOrigin, INotification, ISessionAction, IRootAction, IStateAction, isRootAction, isSessionAction } from '../common/state/sessionActions.js'; +import { ActionType, NotificationType, IActionEnvelope, IActionOrigin, INotification, ISessionAction, IRootAction, IStateAction, isRootAction, isSessionAction } from '../common/state/sessionActions.js'; import type { IStateSnapshot } from '../common/state/sessionProtocol.js'; import { rootReducer, sessionReducer } from '../common/state/sessionReducers.js'; import { createRootState, createSessionState, type IRootState, type ISessionState, type ISessionSummary, type URI, ROOT_STATE_URI } from '../common/state/sessionState.js'; @@ -105,7 +105,7 @@ export class SessionStateManager extends Disposable { this._logService.trace(`[SessionStateManager] Created session: ${key}`); this._onDidEmitNotification.fire({ - type: 'notify/sessionAdded', + type: NotificationType.SessionAdded, summary, }); @@ -130,7 +130,7 @@ export class SessionStateManager extends Disposable { this._logService.trace(`[SessionStateManager] Removed session: ${session}`); this._onDidEmitNotification.fire({ - type: 'notify/sessionRemoved', + type: NotificationType.SessionRemoved, session, }); } @@ -186,16 +186,16 @@ export class SessionStateManager extends Disposable { this._sessionStates.set(key, newState); // Track active turn for turn lifecycle - if (sessionAction.type === 'session/turnStarted') { + if (sessionAction.type === ActionType.SessionTurnStarted) { this._activeTurnToSession.set(sessionAction.turnId, key); - this.dispatchServerAction({ type: 'root/activeSessionsChanged', activeSessions: this._activeTurnToSession.size }); + this.dispatchServerAction({ type: ActionType.RootActiveSessionsChanged, activeSessions: this._activeTurnToSession.size }); } else if ( - sessionAction.type === 'session/turnComplete' || - sessionAction.type === 'session/turnCancelled' || - sessionAction.type === 'session/error' + sessionAction.type === ActionType.SessionTurnComplete || + sessionAction.type === ActionType.SessionTurnCancelled || + sessionAction.type === ActionType.SessionError ) { this._activeTurnToSession.delete(sessionAction.turnId); - this.dispatchServerAction({ type: 'root/activeSessionsChanged', activeSessions: this._activeTurnToSession.size }); + this.dispatchServerAction({ type: ActionType.RootActiveSessionsChanged, activeSessions: this._activeTurnToSession.size }); } resultingState = newState; diff --git a/src/vs/platform/agentHost/test/node/agentEventMapper.test.ts b/src/vs/platform/agentHost/test/node/agentEventMapper.test.ts index e58eac8dece..fe8993fea3e 100644 --- a/src/vs/platform/agentHost/test/node/agentEventMapper.test.ts +++ b/src/vs/platform/agentHost/test/node/agentEventMapper.test.ts @@ -31,6 +31,7 @@ import type { ITurnCompleteAction, IUsageAction, } from '../../common/state/sessionActions.js'; +import { PermissionKind } from '../../common/state/sessionState.js'; import { mapProgressEventToActions } from '../../node/agentEventMapper.js'; /** Helper: flatten the result of mapProgressEventToActions into an array. */ @@ -188,7 +189,7 @@ suite('AgentEventMapper', () => { session, type: 'permission_request', requestId: 'perm-1', - permissionKind: 'shell', + permissionKind: PermissionKind.Shell, toolCallId: 'tc-2', fullCommandText: 'rm -rf /', intention: 'Delete all files', diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 8754a80bb71..dd71dfbaa89 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -10,7 +10,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { NullLogService } from '../../../log/common/log.js'; import { FileService } from '../../../files/common/fileService.js'; import { AgentSession } from '../../common/agentService.js'; -import { IActionEnvelope } from '../../common/state/sessionActions.js'; +import { ActionType, IActionEnvelope } from '../../common/state/sessionActions.js'; import { AgentService } from '../../node/agentService.js'; import { MockAgent } from './mockAgent.js'; @@ -51,7 +51,7 @@ suite('AgentService (node dispatcher)', () => { // Start a turn so there's an active turn to map events to service.dispatchAction( - { type: 'session/turnStarted', session: session.toString(), turnId: 'turn-1', userMessage: { text: 'hello' } }, + { type: ActionType.SessionTurnStarted, session: session.toString(), turnId: 'turn-1', userMessage: { text: 'hello' } }, 'test-client', 1, ); @@ -59,7 +59,7 @@ suite('AgentService (node dispatcher)', () => { disposables.add(service.onDidAction(e => envelopes.push(e))); copilotAgent.fireProgress({ session, type: 'delta', messageId: 'msg-1', content: 'hello' }); - assert.ok(envelopes.some(e => e.action.type === 'session/delta')); + assert.ok(envelopes.some(e => e.action.type === ActionType.SessionDelta)); }); }); @@ -164,7 +164,7 @@ suite('AgentService (node dispatcher)', () => { // Model fetch is async inside AgentSideEffects — wait for it await new Promise(r => setTimeout(r, 50)); - const agentsChanged = envelopes.find(e => e.action.type === 'root/agentsChanged'); + const agentsChanged = envelopes.find(e => e.action.type === ActionType.RootAgentsChanged); assert.ok(agentsChanged); }); }); diff --git a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts index fc1772e5d37..86f9284b2a7 100644 --- a/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts +++ b/src/vs/platform/agentHost/test/node/agentSideEffects.test.ts @@ -14,8 +14,8 @@ import { FileService } from '../../../files/common/fileService.js'; import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js'; import { NullLogService } from '../../../log/common/log.js'; import { AgentSession, IAgent } from '../../common/agentService.js'; -import { IActionEnvelope, ISessionAction } from '../../common/state/sessionActions.js'; -import { SessionStatus } from '../../common/state/sessionState.js'; +import { ActionType, IActionEnvelope, ISessionAction } from '../../common/state/sessionActions.js'; +import { PermissionKind, SessionStatus } from '../../common/state/sessionState.js'; import { AgentSideEffects } from '../../node/agentSideEffects.js'; import { SessionStateManager } from '../../node/sessionStateManager.js'; import { MockAgent } from './mockAgent.js'; @@ -42,12 +42,12 @@ suite('AgentSideEffects', () => { createdAt: Date.now(), modifiedAt: Date.now(), }); - stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri.toString() }); + stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri.toString() }); } function startTurn(turnId: string): void { stateManager.dispatchClientAction( - { type: 'session/turnStarted', session: sessionUri.toString(), turnId, userMessage: { text: 'hello' } }, + { type: ActionType.SessionTurnStarted, session: sessionUri.toString(), turnId, userMessage: { text: 'hello' } }, { clientId: 'test', clientSeq: 1 }, ); } @@ -84,7 +84,7 @@ suite('AgentSideEffects', () => { test('calls sendMessage on the agent', async () => { setupSession(); const action: ISessionAction = { - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello world' }, @@ -109,13 +109,13 @@ suite('AgentSideEffects', () => { disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e))); noAgentSideEffects.handleAction({ - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: sessionUri.toString(), turnId: 'turn-1', userMessage: { text: 'hello' }, }); - const errorAction = envelopes.find(e => e.action.type === 'session/error'); + const errorAction = envelopes.find(e => e.action.type === ActionType.SessionError); assert.ok(errorAction, 'should dispatch session/error'); }); }); @@ -127,7 +127,7 @@ suite('AgentSideEffects', () => { test('calls abortSession on the agent', async () => { setupSession(); sideEffects.handleAction({ - type: 'session/turnCancelled', + type: ActionType.SessionTurnCancelled, session: sessionUri.toString(), turnId: 'turn-1', }); @@ -152,14 +152,14 @@ suite('AgentSideEffects', () => { session: sessionUri, type: 'permission_request', requestId: 'perm-1', - permissionKind: 'write', + permissionKind: PermissionKind.Write, path: 'file.ts', rawRequest: '{}', }); // Now resolve it sideEffects.handleAction({ - type: 'session/permissionResolved', + type: ActionType.SessionPermissionResolved, session: sessionUri.toString(), turnId: 'turn-1', requestId: 'perm-1', @@ -177,7 +177,7 @@ suite('AgentSideEffects', () => { test('calls changeModel on the agent', async () => { setupSession(); sideEffects.handleAction({ - type: 'session/modelChanged', + type: ActionType.SessionModelChanged, session: sessionUri.toString(), model: 'gpt-5', }); @@ -202,7 +202,7 @@ suite('AgentSideEffects', () => { agent.fireProgress({ session: sessionUri, type: 'delta', messageId: 'msg-1', content: 'hi' }); - assert.ok(envelopes.some(e => e.action.type === 'session/delta')); + assert.ok(envelopes.some(e => e.action.type === ActionType.SessionDelta)); }); test('returns a disposable that stops listening', () => { @@ -214,11 +214,11 @@ suite('AgentSideEffects', () => { const listener = sideEffects.registerProgressListener(agent); agent.fireProgress({ session: sessionUri, type: 'delta', messageId: 'msg-1', content: 'before' }); - assert.strictEqual(envelopes.filter(e => e.action.type === 'session/delta').length, 1); + assert.strictEqual(envelopes.filter(e => e.action.type === ActionType.SessionDelta).length, 1); listener.dispose(); agent.fireProgress({ session: sessionUri, type: 'delta', messageId: 'msg-2', content: 'after' }); - assert.strictEqual(envelopes.filter(e => e.action.type === 'session/delta').length, 1); + assert.strictEqual(envelopes.filter(e => e.action.type === ActionType.SessionDelta).length, 1); }); }); @@ -232,7 +232,7 @@ suite('AgentSideEffects', () => { await sideEffects.handleCreateSession({ session: sessionUri.toString(), provider: 'mock' }); - const ready = envelopes.find(e => e.action.type === 'session/ready'); + const ready = envelopes.find(e => e.action.type === ActionType.SessionReady); assert.ok(ready, 'should dispatch session/ready'); }); @@ -318,7 +318,7 @@ suite('AgentSideEffects', () => { // Model fetch is async — wait for it await new Promise(r => setTimeout(r, 50)); - const action = envelopes.find(e => e.action.type === 'root/agentsChanged'); + const action = envelopes.find(e => e.action.type === ActionType.RootAgentsChanged); assert.ok(action, 'should dispatch root/agentsChanged'); }); }); diff --git a/src/vs/platform/agentHost/test/node/mockAgent.ts b/src/vs/platform/agentHost/test/node/mockAgent.ts index 6ddf3ac28c3..bea9ddcff28 100644 --- a/src/vs/platform/agentHost/test/node/mockAgent.ts +++ b/src/vs/platform/agentHost/test/node/mockAgent.ts @@ -6,6 +6,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; import { AgentSession, type AgentProvider, type IAgent, type IAgentAttachment, type IAgentCreateSessionConfig, type IAgentDescriptor, type IAgentMessageEvent, type IAgentModelInfo, type IAgentProgressEvent, type IAgentSessionMetadata, type IAgentToolCompleteEvent, type IAgentToolStartEvent } from '../../common/agentService.js'; +import { PermissionKind } from '../../common/state/sessionState.js'; /** * General-purpose mock agent for unit tests. Tracks all method calls @@ -149,10 +150,10 @@ export class ScriptedMockAgent implements IAgent { type: 'permission_request', session, requestId: 'perm-1', - permissionKind: 'shell', + permissionKind: PermissionKind.Shell, fullCommandText: 'echo test', intention: 'Run a test command', - rawRequest: JSON.stringify({ permissionKind: 'shell', fullCommandText: 'echo test', intention: 'Run a test command' }), + rawRequest: JSON.stringify({ permissionKind: PermissionKind.Shell, fullCommandText: 'echo test', intention: 'Run a test command' }), }; setTimeout(() => this._onDidSessionProgress.fire(permEvent), 10); this._pendingPermissions.set('perm-1', (approved) => { diff --git a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts index 3c6a3b83fed..9385ce8f6b7 100644 --- a/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts +++ b/src/vs/platform/agentHost/test/node/protocolServerHandler.test.ts @@ -9,7 +9,7 @@ import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../log/common/log.js'; -import type { ISessionAction } from '../../common/state/sessionActions.js'; +import { ActionType, type ISessionAction } from '../../common/state/sessionActions.js'; import { isJsonRpcNotification, isJsonRpcResponse, JSON_RPC_INTERNAL_ERROR, ProtocolError, type ICreateSessionParams, type IInitializeResult, type IProtocolMessage, type IAhpNotification, type IReconnectResult, type IStateSnapshot } from '../../common/state/sessionProtocol.js'; import { SessionStatus, type ISessionSummary } from '../../common/state/sessionState.js'; import { PROTOCOL_VERSION } from '../../common/state/sessionCapabilities.js'; @@ -206,7 +206,7 @@ suite('ProtocolServerHandler', () => { test('client action is dispatched and echoed', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); const transport = connectClient('client-1', [sessionUri]); transport.sent.length = 0; @@ -214,7 +214,7 @@ suite('ProtocolServerHandler', () => { transport.simulateMessage(notification('dispatchAction', { clientSeq: 1, action: { - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, @@ -224,7 +224,7 @@ suite('ProtocolServerHandler', () => { const actionMsgs = findNotifications(transport.sent, 'action'); const turnStarted = actionMsgs.find(m => { const envelope = m.params as unknown as { action: { type: string } }; - return envelope.action.type === 'session/turnStarted'; + return envelope.action.type === ActionType.SessionTurnStarted; }); assert.ok(turnStarted, 'should have echoed turnStarted'); const envelope = turnStarted!.params as unknown as { origin: { clientId: string; clientSeq: number } }; @@ -234,7 +234,7 @@ suite('ProtocolServerHandler', () => { test('actions are scoped to subscribed sessions', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); const transportA = connectClient('client-a', [sessionUri]); const transportB = connectClient('client-b'); @@ -243,7 +243,7 @@ suite('ProtocolServerHandler', () => { transportB.sent.length = 0; stateManager.dispatchServerAction({ - type: 'session/titleChanged', + type: ActionType.SessionTitleChanged, session: sessionUri, title: 'New Title', }); @@ -267,15 +267,15 @@ suite('ProtocolServerHandler', () => { test('reconnect replays missed actions', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); const transport1 = connectClient('client-r', [sessionUri]); const resp = findResponse(transport1.sent, 1); const initSeq = (resp as { result: IInitializeResult }).result.serverSeq; transport1.simulateClose(); - stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'Title A' }); - stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'Title B' }); + stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Title A' }); + stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Title B' }); const transport2 = new MockProtocolTransport(); server.simulateConnection(transport2); @@ -296,13 +296,13 @@ suite('ProtocolServerHandler', () => { test('reconnect sends fresh snapshots when gap too large', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); const transport1 = connectClient('client-g', [sessionUri]); transport1.simulateClose(); for (let i = 0; i < 1100; i++) { - stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: `Title ${i}` }); + stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: `Title ${i}` }); } const transport2 = new MockProtocolTransport(); @@ -324,14 +324,14 @@ suite('ProtocolServerHandler', () => { test('client disconnect cleans up', () => { stateManager.createSession(makeSessionSummary()); - stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); const transport = connectClient('client-d', [sessionUri]); transport.sent.length = 0; transport.simulateClose(); - stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'After Disconnect' }); + stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'After Disconnect' }); assert.strictEqual(transport.sent.length, 0); }); diff --git a/src/vs/platform/agentHost/test/node/sessionStateManager.test.ts b/src/vs/platform/agentHost/test/node/sessionStateManager.test.ts index 989f61d6338..10b2db4d8c9 100644 --- a/src/vs/platform/agentHost/test/node/sessionStateManager.test.ts +++ b/src/vs/platform/agentHost/test/node/sessionStateManager.test.ts @@ -8,7 +8,7 @@ import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { NullLogService } from '../../../log/common/log.js'; -import type { IActionEnvelope, INotification } from '../../common/state/sessionActions.js'; +import { ActionType, NotificationType, type IActionEnvelope, type INotification } from '../../common/state/sessionActions.js'; import { ISessionSummary, ROOT_STATE_URI, SessionLifecycle, SessionStatus, type ISessionState } from '../../common/state/sessionState.js'; import { SessionStateManager } from '../../node/sessionStateManager.js'; @@ -76,7 +76,7 @@ suite('SessionStateManager', () => { disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); manager.dispatchServerAction({ - type: 'session/ready', + type: ActionType.SessionReady, session: sessionUri, }); @@ -85,7 +85,7 @@ suite('SessionStateManager', () => { assert.strictEqual(state.lifecycle, SessionLifecycle.Ready); assert.strictEqual(envelopes.length, 1); - assert.strictEqual(envelopes[0].action.type, 'session/ready'); + assert.strictEqual(envelopes[0].action.type, ActionType.SessionReady); assert.strictEqual(envelopes[0].serverSeq, 1); assert.strictEqual(envelopes[0].origin, undefined); }); @@ -96,8 +96,8 @@ suite('SessionStateManager', () => { const envelopes: IActionEnvelope[] = []; disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); - manager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); - manager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'Updated' }); + manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Updated' }); assert.strictEqual(envelopes.length, 2); assert.strictEqual(envelopes[0].serverSeq, 1); @@ -113,7 +113,7 @@ suite('SessionStateManager', () => { const origin = { clientId: 'renderer-1', clientSeq: 42 }; manager.dispatchClientAction( - { type: 'session/ready', session: sessionUri }, + { type: ActionType.SessionReady, session: sessionUri }, origin, ); @@ -132,7 +132,7 @@ suite('SessionStateManager', () => { assert.strictEqual(manager.getSessionState(sessionUri), undefined); assert.strictEqual(manager.getSnapshot(sessionUri), undefined); assert.strictEqual(notifications.length, 1); - assert.strictEqual(notifications[0].type, 'notify/sessionRemoved'); + assert.strictEqual(notifications[0].type, NotificationType.SessionRemoved); }); test('createSession emits sessionAdded notification', () => { @@ -142,17 +142,17 @@ suite('SessionStateManager', () => { manager.createSession(makeSessionSummary()); assert.strictEqual(notifications.length, 1); - assert.strictEqual(notifications[0].type, 'notify/sessionAdded'); + assert.strictEqual(notifications[0].type, NotificationType.SessionAdded); }); test('getActiveTurnId returns active turn id after turnStarted', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); assert.strictEqual(manager.getActiveTurnId(sessionUri), undefined); manager.dispatchServerAction({ - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, @@ -169,19 +169,19 @@ suite('SessionStateManager', () => { test('turnStarted dispatches root/activeSessionsChanged with correct count', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); const envelopes: IActionEnvelope[] = []; disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); manager.dispatchServerAction({ - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, }); - const activeChanged = envelopes.filter(e => e.action.type === 'root/activeSessionsChanged'); + const activeChanged = envelopes.filter(e => e.action.type === ActionType.RootActiveSessionsChanged); assert.strictEqual(activeChanged.length, 1); assert.strictEqual((activeChanged[0].action as { activeSessions: number }).activeSessions, 1); assert.strictEqual(manager.rootState.activeSessions, 1); @@ -189,9 +189,9 @@ suite('SessionStateManager', () => { test('turnComplete dispatches root/activeSessionsChanged back to 0', () => { manager.createSession(makeSessionSummary()); - manager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); + manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); manager.dispatchServerAction({ - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: sessionUri, turnId: 'turn-1', userMessage: { text: 'hello' }, @@ -201,12 +201,12 @@ suite('SessionStateManager', () => { disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e))); manager.dispatchServerAction({ - type: 'session/turnComplete', + type: ActionType.SessionTurnComplete, session: sessionUri, turnId: 'turn-1', }); - const activeChanged = envelopes.filter(e => e.action.type === 'root/activeSessionsChanged'); + const activeChanged = envelopes.filter(e => e.action.type === ActionType.RootActiveSessionsChanged); assert.strictEqual(activeChanged.length, 1); assert.strictEqual((activeChanged[0].action as { activeSessions: number }).activeSessions, 0); assert.strictEqual(manager.rootState.activeSessions, 0); @@ -216,17 +216,17 @@ suite('SessionStateManager', () => { const session2Uri = URI.from({ scheme: 'copilot', path: '/test-session-2' }).toString(); manager.createSession(makeSessionSummary(sessionUri)); manager.createSession(makeSessionSummary(session2Uri)); - manager.dispatchServerAction({ type: 'session/ready', session: sessionUri }); - manager.dispatchServerAction({ type: 'session/ready', session: session2Uri }); + manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri }); + manager.dispatchServerAction({ type: ActionType.SessionReady, session: session2Uri }); manager.dispatchServerAction({ - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: sessionUri, turnId: 'turn-1', userMessage: { text: 'a' }, }); manager.dispatchServerAction({ - type: 'session/turnStarted', + type: ActionType.SessionTurnStarted, session: session2Uri, turnId: 'turn-2', userMessage: { text: 'b' }, @@ -234,14 +234,14 @@ suite('SessionStateManager', () => { assert.strictEqual(manager.rootState.activeSessions, 2); manager.dispatchServerAction({ - type: 'session/turnComplete', + type: ActionType.SessionTurnComplete, session: sessionUri, turnId: 'turn-1', }); assert.strictEqual(manager.rootState.activeSessions, 1); manager.dispatchServerAction({ - type: 'session/turnComplete', + type: ActionType.SessionTurnComplete, session: session2Uri, turnId: 'turn-2', }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts index a1ddeacc81b..6bd95dca882 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts @@ -16,10 +16,10 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { IProductService } from '../../../../../../platform/product/common/productService.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IAgentAttachment, AgentProvider, AgentSession, type IAgentConnection } from '../../../../../../platform/agentHost/common/agentService.js'; -import { isSessionAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; +import { ActionType, isSessionAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import { SessionClientState } from '../../../../../../platform/agentHost/common/state/sessionClientState.js'; import { getToolKind, getToolLanguage } from '../../../../../../platform/agentHost/common/state/sessionReducers.js'; -import { TurnState, type IMessageAttachment } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { AttachmentType, ToolCallStatus, TurnState, type IMessageAttachment } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; import { IChatAgentData, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../../common/participants/chatAgents.js'; import { IChatProgress, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; @@ -265,7 +265,7 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC const currentModel = this._clientState.getSessionState(session.toString())?.summary.model; if (currentModel !== rawModelId) { const modelAction = { - type: 'session/modelChanged' as const, + type: ActionType.SessionModelChanged as const, session: session.toString(), model: rawModelId, }; @@ -277,7 +277,7 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC // Dispatch session/turnStarted — the server will call sendMessage on // the provider as a side effect. const turnAction = { - type: 'session/turnStarted' as const, + type: ActionType.SessionTurnStarted as const, session: session.toString(), turnId, userMessage: { @@ -355,15 +355,15 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC for (const [toolCallId, tc] of Object.entries(activeTurn.toolCalls)) { const existing = activeToolInvocations.get(toolCallId); if (!existing) { - if (tc.status === 'running' || tc.status === 'streaming' || tc.status === 'pending-confirmation') { + if (tc.status === ToolCallStatus.Running || tc.status === ToolCallStatus.Streaming || tc.status === ToolCallStatus.PendingConfirmation) { const invocation = toolCallStateToInvocation(tc); activeToolInvocations.set(toolCallId, invocation); progress([invocation]); } - } else if (tc.status === 'completed' || tc.status === 'cancelled') { + } else if (tc.status === ToolCallStatus.Completed || tc.status === ToolCallStatus.Cancelled) { activeToolInvocations.delete(toolCallId); finalizeToolInvocation(existing, tc); - } else if (tc.status === 'running' || tc.status === 'pending-confirmation') { + } else if (tc.status === ToolCallStatus.Running || tc.status === ToolCallStatus.PendingConfirmation) { // Tool transitioned from streaming to ready — update the invocation // with the now-available invocationMessage and toolSpecificData. existing.invocationMessage = typeof tc.invocationMessage === 'string' @@ -392,7 +392,7 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC const approved = reason.type !== ToolConfirmKind.Denied && reason.type !== ToolConfirmKind.Skipped; this._logService.info(`[AgentHost] Permission response: requestId=${requestId}, approved=${approved}`); const resolveAction = { - type: 'session/permissionResolved' as const, + type: ActionType.SessionPermissionResolved as const, session: session.toString(), turnId, requestId, @@ -414,7 +414,7 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC turnDisposables.add(cancellationToken.onCancellationRequested(() => { this._logService.info(`[AgentHost] Cancellation requested for ${session.toString()}, dispatching turnCancelled`); const cancelAction = { - type: 'session/turnCancelled' as const, + type: ActionType.SessionTurnCancelled as const, session: session.toString(), turnId, }; @@ -481,17 +481,17 @@ export class AgentHostSessionHandler extends Disposable implements IChatSessionC if (v.kind === 'file') { const uri = v.value instanceof URI ? v.value : undefined; if (uri?.scheme === 'file') { - attachments.push({ type: 'file', path: uri.fsPath, displayName: v.name }); + attachments.push({ type: AttachmentType.File, path: uri.fsPath, displayName: v.name }); } } else if (v.kind === 'directory') { const uri = v.value instanceof URI ? v.value : undefined; if (uri?.scheme === 'file') { - attachments.push({ type: 'directory', path: uri.fsPath, displayName: v.name }); + attachments.push({ type: AttachmentType.Directory, path: uri.fsPath, displayName: v.name }); } } else if (v.kind === 'implicit' && v.isSelection) { const uri = v.uri; if (uri?.scheme === 'file') { - attachments.push({ type: 'selection', path: uri.fsPath, displayName: v.name }); + attachments.push({ type: AttachmentType.Selection, path: uri.fsPath, displayName: v.name }); } } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts index 7b5cf77c3dd..bad97cd2521 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js'; -import { TurnState, getToolOutputText, type ICompletedToolCall, type IPermissionRequest, type IToolCallState, type ITurn } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { PermissionKind, ToolCallStatus, TurnState, getToolOutputText, type ICompletedToolCall, type IPermissionRequest, type IToolCallState, type ITurn } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { getToolKind, getToolLanguage } from '../../../../../../platform/agentHost/common/state/sessionReducers.js'; import { type IChatProgress, type IChatTerminalToolInvocationData, type IChatToolInputInvocationData, type IChatToolInvocationSerialized, ToolConfirmKind } from '../../../common/chatService/chatService.js'; import { type IChatSessionHistoryItem } from '../../../common/chatSessionsService.js'; @@ -49,12 +49,12 @@ export function turnsToHistory(turns: readonly ITurn[], participantId: string): */ function completedToolCallToSerialized(tc: ICompletedToolCall): IChatToolInvocationSerialized { const isTerminal = getToolKind(tc) === 'terminal'; - const isSuccess = tc.status === 'completed' && tc.success; + const isSuccess = tc.status === ToolCallStatus.Completed && tc.success; const invocationMsg = stringOrMarkdownToString(tc.invocationMessage) ?? ''; let toolSpecificData: IChatTerminalToolInvocationData | undefined; if (isTerminal && tc.toolInput) { - const toolOutput = tc.status === 'completed' ? getToolOutputText(tc) : undefined; + const toolOutput = tc.status === ToolCallStatus.Completed ? getToolOutputText(tc) : undefined; toolSpecificData = { kind: 'terminal', commandLine: { original: tc.toolInput }, @@ -117,7 +117,7 @@ export function toolCallStateToInvocation(tc: IToolCallState): ChatToolInvocatio if (getToolKind(tc) === 'terminal') { invocation.toolSpecificData = { kind: 'terminal', - commandLine: { original: tc.status !== 'streaming' ? (tc.toolInput ?? '') : '' }, + commandLine: { original: tc.status !== ToolCallStatus.Streaming ? (tc.toolInput ?? '') : '' }, language: getToolLanguage(tc) ?? 'shellscript', } satisfies IChatTerminalToolInvocationData; } @@ -135,7 +135,7 @@ export function permissionToConfirmation(perm: IPermissionRequest): ChatToolInvo let toolSpecificData: IChatTerminalToolInvocationData | IChatToolInputInvocationData | undefined; switch (perm.permissionKind) { - case 'shell': { + case PermissionKind.Shell: { title = perm.intention ?? 'Run command'; toolSpecificData = perm.fullCommandText ? { kind: 'terminal', @@ -144,14 +144,14 @@ export function permissionToConfirmation(perm: IPermissionRequest): ChatToolInvo } : undefined; break; } - case 'write': { + case PermissionKind.Write: { title = perm.path ? `Edit ${perm.path}` : 'Edit file'; let rawInput: unknown; try { rawInput = perm.rawRequest ? JSON.parse(perm.rawRequest) : { path: perm.path }; } catch { rawInput = { path: perm.path }; } toolSpecificData = { kind: 'input', rawInput }; break; } - case 'mcp': { + case PermissionKind.Mcp: { const toolTitle = perm.toolName ?? 'MCP Tool'; title = perm.serverName ? `${perm.serverName}: ${toolTitle}` : toolTitle; let rawInput: unknown; @@ -159,7 +159,7 @@ export function permissionToConfirmation(perm: IPermissionRequest): ChatToolInvo toolSpecificData = { kind: 'input', rawInput }; break; } - case 'read': { + case PermissionKind.Read: { title = perm.intention ?? 'Read file'; let rawInput: unknown; try { rawInput = perm.rawRequest ? JSON.parse(perm.rawRequest) : { path: perm.path, intention: perm.intention }; } catch { rawInput = { path: perm.path, intention: perm.intention }; } @@ -202,8 +202,8 @@ export function permissionToConfirmation(perm: IPermissionRequest): ChatToolInvo * protocol's tool-call state, transitioning it to the completed state. */ export function finalizeToolInvocation(invocation: ChatToolInvocation, tc: IToolCallState): void { - const isCompleted = tc.status === 'completed'; - const isCancelled = tc.status === 'cancelled'; + const isCompleted = tc.status === ToolCallStatus.Completed; + const isCancelled = tc.status === ToolCallStatus.Cancelled; const isTerminal = invocation.toolSpecificData?.kind === 'terminal' || getToolKind(tc) === 'terminal'; if (isTerminal && (isCompleted || isCancelled)) { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts index 36256611e93..07344ef9636 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts @@ -16,7 +16,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { IAgentCreateSessionConfig, IAgentHostService, IAgentSessionMetadata, AgentSession } from '../../../../../../platform/agentHost/common/agentService.js'; import type { IActionEnvelope, INotification, IPermissionResolvedAction, ISessionAction, ITurnStartedAction } from '../../../../../../platform/agentHost/common/state/sessionActions.js'; import type { IStateSnapshot } from '../../../../../../platform/agentHost/common/state/sessionProtocol.js'; -import { SessionLifecycle, SessionStatus, TurnState, createSessionState, ROOT_STATE_URI, type ISessionState, type ISessionSummary } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { SessionLifecycle, SessionStatus, TurnState, createSessionState, ROOT_STATE_URI, PolicyState, type ISessionState, type ISessionSummary } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { IDefaultAccountService } from '../../../../../../platform/defaultAccount/common/defaultAccount.js'; import { IAuthenticationService } from '../../../../../services/authentication/common/authentication.js'; import { IChatAgentData, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../../common/participants/chatAgents.js'; @@ -1181,8 +1181,8 @@ suite('AgentHostChatContribution', () => { test('filters out disabled models', async () => { const provider = disposables.add(new AgentHostLanguageModelProvider('agent-host-copilot', 'agent-host-copilot')); provider.updateModels([ - { provider: 'copilot', id: 'gpt-4o', name: 'GPT-4o', maxContextWindow: 128000, supportsVision: false, policyState: 'enabled' }, - { provider: 'copilot', id: 'gpt-3.5', name: 'GPT-3.5', maxContextWindow: 16000, supportsVision: false, policyState: 'disabled' }, + { provider: 'copilot', id: 'gpt-4o', name: 'GPT-4o', maxContextWindow: 128000, supportsVision: false, policyState: PolicyState.Enabled }, + { provider: 'copilot', id: 'gpt-3.5', name: 'GPT-3.5', maxContextWindow: 16000, supportsVision: false, policyState: PolicyState.Disabled }, ]); const models = await provider.provideLanguageModelChatInfo({}, CancellationToken.None); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts index 19b43fdf7eb..aa4337eaa49 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/stateToProgressAdapter.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { TurnState, type ICompletedToolCall, type IPermissionRequest, type IToolCallRunningState, type ITurn } from '../../../../../../platform/agentHost/common/state/sessionState.js'; +import { ToolCallStatus, ToolCallConfirmationReason, PermissionKind, ToolResultContentType, TurnState, type ICompletedToolCall, type IPermissionRequest, type IToolCallRunningState, type ITurn } from '../../../../../../platform/agentHost/common/state/sessionState.js'; import { IChatToolInvocationSerialized, type IChatMarkdownContent } from '../../../common/chatService/chatService.js'; import { ToolDataSource } from '../../../common/tools/languageModelToolsService.js'; import { turnsToHistory, toolCallStateToInvocation, permissionToConfirmation, finalizeToolInvocation } from '../../../browser/agentSessions/agentHost/stateToProgressAdapter.js'; @@ -18,21 +18,21 @@ function createToolCallState(overrides?: Partial): IToolC toolName: 'test_tool', displayName: 'Test Tool', invocationMessage: 'Running test tool...', - status: 'running', - confirmed: 'not-needed', + status: ToolCallStatus.Running, + confirmed: ToolCallConfirmationReason.NotNeeded, ...overrides, }; } function createCompletedToolCall(overrides?: Partial): ICompletedToolCall { return { - status: 'completed', + status: ToolCallStatus.Completed, toolCallId: 'tc-1', toolName: 'test_tool', displayName: 'Test Tool', invocationMessage: 'Running test tool...', success: true, - confirmed: 'not-needed', + confirmed: ToolCallConfirmationReason.NotNeeded, pastTenseMessage: 'Ran test tool', ...overrides, } as ICompletedToolCall; @@ -54,7 +54,7 @@ function createTurn(overrides?: Partial): ITurn { function createPermission(overrides?: Partial): IPermissionRequest { return { requestId: 'perm-1', - permissionKind: 'shell', + permissionKind: PermissionKind.Shell, ...overrides, }; } @@ -103,7 +103,7 @@ suite('stateToProgressAdapter', () => { toolCalls: [createCompletedToolCall({ _meta: { toolKind: 'terminal', language: 'shellscript' }, toolInput: 'echo hello', - content: [{ type: 'text', text: 'hello' }], + content: [{ type: ToolResultContentType.Text, text: 'hello' }], success: true, })], }); @@ -157,7 +157,7 @@ suite('stateToProgressAdapter', () => { toolCalls: [createCompletedToolCall({ _meta: { toolKind: 'terminal' }, toolInput: 'bad-command', - content: [{ type: 'text', text: 'error' }], + content: [{ type: ToolResultContentType.Text, text: 'error' }], success: false, })], }); @@ -183,7 +183,7 @@ suite('stateToProgressAdapter', () => { toolName: 'my_tool', displayName: 'My Tool', invocationMessage: 'Doing stuff', - status: 'running', + status: ToolCallStatus.Running, }); const invocation = toolCallStateToInvocation(tc); @@ -217,7 +217,7 @@ suite('stateToProgressAdapter', () => { test('shell permission has terminal data', () => { const perm = createPermission({ - permissionKind: 'shell', + permissionKind: PermissionKind.Shell, fullCommandText: 'rm -rf /', intention: 'Delete everything', }); @@ -231,7 +231,7 @@ suite('stateToProgressAdapter', () => { test('mcp permission uses server + tool name as title', () => { const perm = createPermission({ - permissionKind: 'mcp', + permissionKind: PermissionKind.Mcp, serverName: 'My Server', toolName: 'my_tool', }); @@ -243,7 +243,7 @@ suite('stateToProgressAdapter', () => { test('write permission has input data', () => { const perm = createPermission({ - permissionKind: 'write', + permissionKind: PermissionKind.Write, path: '/test.ts', rawRequest: '{"path":"/test.ts","content":"hello"}', }); @@ -260,22 +260,22 @@ suite('stateToProgressAdapter', () => { const tc = createToolCallState({ _meta: { toolKind: 'terminal' }, toolInput: 'echo hi', - status: 'running', + status: ToolCallStatus.Running, }); const invocation = toolCallStateToInvocation(tc); finalizeToolInvocation(invocation, { - status: 'completed', + status: ToolCallStatus.Completed, toolCallId: 'tc-1', toolName: 'test_tool', displayName: 'Test Tool', invocationMessage: 'Running test tool...', _meta: { toolKind: 'terminal' }, toolInput: 'echo hi', - confirmed: 'not-needed', + confirmed: ToolCallConfirmationReason.NotNeeded, success: true, pastTenseMessage: 'Ran echo hi', - content: [{ type: 'text', text: 'output text' }], + content: [{ type: ToolResultContentType.Text, text: 'output text' }], }); assert.ok(invocation.toolSpecificData); @@ -287,17 +287,17 @@ suite('stateToProgressAdapter', () => { test('finalizes failed tool with error message', () => { const tc = createToolCallState({ - status: 'running', + status: ToolCallStatus.Running, }); const invocation = toolCallStateToInvocation(tc); finalizeToolInvocation(invocation, { - status: 'completed', + status: ToolCallStatus.Completed, toolCallId: 'tc-1', toolName: 'test_tool', displayName: 'Test Tool', invocationMessage: 'Running test tool...', - confirmed: 'not-needed', + confirmed: ToolCallConfirmationReason.NotNeeded, success: false, pastTenseMessage: 'Failed', error: { message: 'timeout' },