agentHost: upstream reducer logic from AHP

Goes with https://github.com/microsoft/agent-host-protocol/pull/11
This commit is contained in:
Connor Peet
2026-03-18 13:09:11 -07:00
parent a185beaf10
commit 60d6da9614
29 changed files with 974 additions and 938 deletions

View File

@@ -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<string, Map<string, string>>();
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<string, string>();
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);

View File

@@ -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). */

View File

@@ -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,
};

View File

@@ -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) */

View File

@@ -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.

View File

@@ -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 ─────────────────────────────────────────────────

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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];
}

View File

@@ -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

View File

@@ -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,
};
/**

View File

@@ -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;

View File

@@ -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) --------------

View File

@@ -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,
};

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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<boolean>();
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({

View File

@@ -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;

View File

@@ -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',

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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) => {

View File

@@ -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);
});

View File

@@ -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',
});

View File

@@ -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 });
}
}
}

View File

@@ -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)) {

View File

@@ -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);

View File

@@ -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<IToolCallRunningState>): 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>): 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>): ITurn {
function createPermission(overrides?: Partial<IPermissionRequest>): 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' },