agentHost: migrate to use protocol types

- Migrates to use AHP types that are synced via `npx tsx scripts/sync-agent-host-protocol.ts`
- One big churn was migrating out of URIs as rich objects in the protocol.
  We can't really shove our own `$mid`-type objects in there. I also explored doing a
  generated translation layer but I had trouble getting one I was happy with.
- This tightens up some type safety in general and fixes some areas where vscode had
  silently/sloppily diverged from the protocol types.
This commit is contained in:
Connor Peet
2026-03-17 20:10:45 -07:00
parent 888a6c5ae6
commit 2418b24d9f
40 changed files with 3304 additions and 1844 deletions

View File

@@ -56,7 +56,7 @@ suite('AgentEventMapper', () => {
content: 'hello world',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
const action = actions[0];
assert.strictEqual(action.type, 'session/delta');
@@ -79,7 +79,7 @@ suite('AgentEventMapper', () => {
language: 'shellscript',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 2);
const startAction = actions[0] as IToolCallStartAction;
@@ -87,8 +87,8 @@ suite('AgentEventMapper', () => {
assert.strictEqual(startAction.toolCallId, 'tc-1');
assert.strictEqual(startAction.toolName, 'readFile');
assert.strictEqual(startAction.displayName, 'Read File');
assert.strictEqual(startAction.toolKind, 'terminal');
assert.strictEqual(startAction.language, 'shellscript');
assert.strictEqual(startAction._meta?.toolKind, 'terminal');
assert.strictEqual(startAction._meta?.language, 'shellscript');
const readyAction = actions[1] as IToolCallReadyAction;
assert.strictEqual(readyAction.type, 'session/toolCallReady');
@@ -108,14 +108,14 @@ suite('AgentEventMapper', () => {
toolOutput: 'file contents here',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
const complete = actions[0] as IToolCallCompleteAction;
assert.strictEqual(complete.type, 'session/toolCallComplete');
assert.strictEqual(complete.toolCallId, 'tc-1');
assert.strictEqual(complete.result.success, true);
assert.strictEqual(complete.result.pastTenseMessage, 'Read file successfully');
assert.strictEqual(complete.result.toolOutput, 'file contents here');
assert.deepStrictEqual(complete.result.content, [{ type: 'text', text: 'file contents here' }]);
});
test('idle event maps to session/turnComplete action', () => {
@@ -124,7 +124,7 @@ suite('AgentEventMapper', () => {
type: 'idle',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
const turnComplete = actions[0] as ITurnCompleteAction;
assert.strictEqual(turnComplete.type, 'session/turnComplete');
@@ -141,7 +141,7 @@ suite('AgentEventMapper', () => {
stack: 'Error: Something went wrong\n at foo.ts:1',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
const errorAction = actions[0] as ISessionErrorAction;
assert.strictEqual(errorAction.type, 'session/error');
@@ -160,7 +160,7 @@ suite('AgentEventMapper', () => {
cacheReadTokens: 25,
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
const usageAction = actions[0] as IUsageAction;
assert.strictEqual(usageAction.type, 'session/usage');
@@ -177,7 +177,7 @@ suite('AgentEventMapper', () => {
title: 'New Title',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].type, 'session/titleChanged');
assert.strictEqual((actions[0] as ITitleChangedAction).title, 'New Title');
@@ -195,7 +195,7 @@ suite('AgentEventMapper', () => {
rawRequest: '{}',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].type, 'session/permissionRequest');
const req = (actions[0] as IPermissionRequestAction).request;
@@ -213,7 +213,7 @@ suite('AgentEventMapper', () => {
content: 'Let me think about this...',
};
const actions = mapToArray(mapProgressEventToActions(event, session, turnId));
const actions = mapToArray(mapProgressEventToActions(event, session.toString(), turnId));
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].type, 'session/reasoning');
const reasoning = actions[0] as IReasoningAction;
@@ -230,7 +230,7 @@ suite('AgentEventMapper', () => {
content: 'Some full message',
};
const result = mapProgressEventToActions(event, session, turnId);
const result = mapProgressEventToActions(event, session.toString(), turnId);
assert.strictEqual(result, undefined);
});
});

View File

@@ -35,19 +35,19 @@ suite('AgentSideEffects', () => {
function setupSession(): void {
stateManager.createSession({
resource: sessionUri,
resource: sessionUri.toString(),
provider: 'mock',
title: 'Test',
status: SessionStatus.Idle,
createdAt: Date.now(),
modifiedAt: Date.now(),
});
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri.toString() });
}
function startTurn(turnId: string): void {
stateManager.dispatchClientAction(
{ type: 'session/turnStarted', session: sessionUri, turnId, userMessage: { text: 'hello' } },
{ type: 'session/turnStarted', session: sessionUri.toString(), turnId, userMessage: { text: 'hello' } },
{ clientId: 'test', clientSeq: 1 },
);
}
@@ -85,7 +85,7 @@ suite('AgentSideEffects', () => {
setupSession();
const action: ISessionAction = {
type: 'session/turnStarted',
session: sessionUri,
session: sessionUri.toString(),
turnId: 'turn-1',
userMessage: { text: 'hello world' },
};
@@ -94,7 +94,7 @@ suite('AgentSideEffects', () => {
// sendMessage is async but fire-and-forget; wait a tick
await new Promise(r => setTimeout(r, 10));
assert.deepStrictEqual(agent.sendMessageCalls, [{ session: sessionUri, prompt: 'hello world' }]);
assert.deepStrictEqual(agent.sendMessageCalls, [{ session: URI.parse(sessionUri.toString()), prompt: 'hello world' }]);
});
test('dispatches session/error when no agent is found', async () => {
@@ -110,7 +110,7 @@ suite('AgentSideEffects', () => {
noAgentSideEffects.handleAction({
type: 'session/turnStarted',
session: sessionUri,
session: sessionUri.toString(),
turnId: 'turn-1',
userMessage: { text: 'hello' },
});
@@ -128,13 +128,13 @@ suite('AgentSideEffects', () => {
setupSession();
sideEffects.handleAction({
type: 'session/turnCancelled',
session: sessionUri,
session: sessionUri.toString(),
turnId: 'turn-1',
});
await new Promise(r => setTimeout(r, 10));
assert.deepStrictEqual(agent.abortSessionCalls, [sessionUri]);
assert.deepStrictEqual(agent.abortSessionCalls, [URI.parse(sessionUri.toString())]);
});
});
@@ -160,7 +160,7 @@ suite('AgentSideEffects', () => {
// Now resolve it
sideEffects.handleAction({
type: 'session/permissionResolved',
session: sessionUri,
session: sessionUri.toString(),
turnId: 'turn-1',
requestId: 'perm-1',
approved: true,
@@ -178,13 +178,13 @@ suite('AgentSideEffects', () => {
setupSession();
sideEffects.handleAction({
type: 'session/modelChanged',
session: sessionUri,
session: sessionUri.toString(),
model: 'gpt-5',
});
await new Promise(r => setTimeout(r, 10));
assert.deepStrictEqual(agent.changeModelCalls, [{ session: sessionUri, model: 'gpt-5' }]);
assert.deepStrictEqual(agent.changeModelCalls, [{ session: URI.parse(sessionUri.toString()), model: 'gpt-5' }]);
});
});
@@ -230,7 +230,7 @@ suite('AgentSideEffects', () => {
const envelopes: IActionEnvelope[] = [];
disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e)));
await sideEffects.handleCreateSession({ session: sessionUri, provider: 'mock' });
await sideEffects.handleCreateSession({ session: sessionUri.toString(), provider: 'mock' });
const ready = envelopes.find(e => e.action.type === 'session/ready');
assert.ok(ready, 'should dispatch session/ready');
@@ -238,7 +238,7 @@ suite('AgentSideEffects', () => {
test('throws when no provider is specified', async () => {
await assert.rejects(
() => sideEffects.handleCreateSession({ session: sessionUri }),
() => sideEffects.handleCreateSession({ session: sessionUri.toString() }),
/No provider specified/,
);
});
@@ -251,7 +251,7 @@ suite('AgentSideEffects', () => {
}, new NullLogService(), fileService));
await assert.rejects(
() => noAgentSideEffects.handleCreateSession({ session: sessionUri, provider: 'nonexistent' }),
() => noAgentSideEffects.handleCreateSession({ session: sessionUri.toString(), provider: 'nonexistent' }),
/No agent registered/,
);
});
@@ -264,12 +264,12 @@ suite('AgentSideEffects', () => {
test('disposes the session on the agent and removes state', async () => {
setupSession();
sideEffects.handleDisposeSession(sessionUri);
sideEffects.handleDisposeSession(sessionUri.toString());
await new Promise(r => setTimeout(r, 10));
assert.strictEqual(agent.disposeSessionCalls.length, 1);
assert.strictEqual(stateManager.getSessionState(sessionUri), undefined);
assert.strictEqual(stateManager.getSessionState(sessionUri.toString()), undefined);
});
});
@@ -292,14 +292,14 @@ suite('AgentSideEffects', () => {
test('throws when the directory does not exist', async () => {
await assert.rejects(
() => sideEffects.handleBrowseDirectory(URI.from({ scheme: Schemas.inMemory, path: '/nonexistent' })),
() => sideEffects.handleBrowseDirectory(URI.from({ scheme: Schemas.inMemory, path: '/nonexistent' }).toString()),
/Directory not found/,
);
});
test('throws when the target is not a directory', async () => {
await assert.rejects(
() => sideEffects.handleBrowseDirectory(URI.from({ scheme: Schemas.inMemory, path: '/testDir/file.txt' })),
() => sideEffects.handleBrowseDirectory(URI.from({ scheme: Schemas.inMemory, path: '/testDir/file.txt' }).toString()),
/Not a directory/,
);
});

View File

@@ -10,7 +10,7 @@ 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 { isJsonRpcNotification, isJsonRpcResponse, JSON_RPC_INTERNAL_ERROR, ProtocolError, type ICreateSessionParams, type IInitializeResult, type IProtocolMessage, type IProtocolNotification, type IReconnectResult, type IStateSnapshot } from '../../common/state/sessionProtocol.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';
import type { IProtocolServer, IProtocolTransport } from '../../common/state/sessionTransport.js';
@@ -71,13 +71,13 @@ class MockSideEffectHandler implements IProtocolSideEffectHandler {
handleAction(action: ISessionAction): void {
this.handledActions.push(action);
}
async handleCreateSession(_command: ICreateSessionParams): Promise<URI> { return URI.parse('copilot:/mock-session'); }
handleDisposeSession(_session: URI): void { }
async handleCreateSession(_command: ICreateSessionParams): Promise<void> { /* session created via state manager */ }
handleDisposeSession(_session: string): void { }
async handleListSessions(): Promise<ISessionSummary[]> { return []; }
handleSetAuthToken(_token: string): void { }
async handleBrowseDirectory(uri: URI): Promise<{ entries: { name: string; type: 'file' | 'directory' }[] }> {
this.browsedUris.push(uri);
const error = this.browseErrors.get(uri.toString());
async handleBrowseDirectory(uri: string): Promise<{ entries: { name: string; type: 'file' | 'directory' }[] }> {
this.browsedUris.push(URI.parse(uri));
const error = this.browseErrors.get(uri);
if (error) {
throw error;
}
@@ -88,8 +88,8 @@ class MockSideEffectHandler implements IProtocolSideEffectHandler {
],
};
}
getDefaultDirectory(): URI {
return URI.file('/home/testuser');
getDefaultDirectory(): string {
return URI.file('/home/testuser').toString();
}
}
@@ -103,8 +103,8 @@ function request(id: number, method: string, params?: unknown): IProtocolMessage
return { jsonrpc: '2.0', id, method, params } as IProtocolMessage;
}
function findNotifications(sent: IProtocolMessage[], method: string): IProtocolNotification[] {
return sent.filter(isJsonRpcNotification) as IProtocolNotification[];
function findNotifications(sent: IProtocolMessage[], method: string): IAhpNotification[] {
return sent.filter(isJsonRpcNotification) as IAhpNotification[];
}
function findResponse(sent: IProtocolMessage[], id: number): IProtocolMessage | undefined {
@@ -124,9 +124,9 @@ suite('ProtocolServerHandler', () => {
let server: MockProtocolServer;
let sideEffects: MockSideEffectHandler;
const sessionUri = URI.from({ scheme: 'copilot', path: '/test-session' });
const sessionUri = URI.from({ scheme: 'copilot', path: '/test-session' }).toString();
function makeSessionSummary(resource?: URI): ISessionSummary {
function makeSessionSummary(resource?: string): ISessionSummary {
return {
resource: resource ?? sessionUri,
provider: 'copilot',
@@ -137,7 +137,7 @@ suite('ProtocolServerHandler', () => {
};
}
function connectClient(clientId: string, initialSubscriptions?: readonly URI[]): MockProtocolTransport {
function connectClient(clientId: string, initialSubscriptions?: readonly string[]): MockProtocolTransport {
const transport = new MockProtocolTransport();
server.simulateConnection(transport);
transport.simulateMessage(request(1, 'initialize', {
@@ -200,8 +200,8 @@ suite('ProtocolServerHandler', () => {
const resp = await responsePromise;
assert.ok(resp, 'should have sent response');
const snapshot = (resp as { result: IStateSnapshot }).result;
assert.strictEqual(snapshot.resource.toString(), sessionUri.toString());
const result = (resp as unknown as { result: { snapshot: IStateSnapshot } }).result;
assert.strictEqual(result.snapshot.resource.toString(), sessionUri.toString());
});
test('client action is dispatched and echoed', () => {
@@ -223,11 +223,11 @@ suite('ProtocolServerHandler', () => {
const actionMsgs = findNotifications(transport.sent, 'action');
const turnStarted = actionMsgs.find(m => {
const params = m.params as { envelope: { action: { type: string } } };
return params.envelope.action.type === 'session/turnStarted';
const envelope = m.params as unknown as { action: { type: string } };
return envelope.action.type === 'session/turnStarted';
});
assert.ok(turnStarted, 'should have echoed turnStarted');
const envelope = (turnStarted!.params as { envelope: { origin: { clientId: string; clientSeq: number } } }).envelope;
const envelope = turnStarted!.params as unknown as { origin: { clientId: string; clientSeq: number } };
assert.strictEqual(envelope.origin.clientId, 'client-1');
assert.strictEqual(envelope.origin.clientSeq, 1);
});
@@ -342,14 +342,14 @@ suite('ProtocolServerHandler', () => {
const resp = findResponse(transport.sent, 1);
assert.ok(resp);
const result = (resp as { result: IInitializeResult }).result;
assert.strictEqual(URI.revive(result.defaultDirectory!).path, '/home/testuser');
assert.strictEqual(URI.parse(result.defaultDirectory!).path, '/home/testuser');
});
test('browseDirectory routes to side effect handler', async () => {
const transport = connectClient('client-browse');
transport.sent.length = 0;
const dirUri = URI.file('/home/user/project');
const dirUri = URI.file('/home/user/project').toString();
const responsePromise = waitForResponse(transport, 2);
transport.simulateMessage(request(2, 'browseDirectory', { uri: dirUri }));
const resp = await responsePromise;
@@ -358,7 +358,7 @@ suite('ProtocolServerHandler', () => {
assert.strictEqual(sideEffects.browsedUris[0].path, '/home/user/project');
assert.ok(resp);
const result = (resp as { result: { entries: { name: string; uri: unknown; type: string }[] } }).result;
const result = (resp as unknown as { result: { entries: { name: string; uri: unknown; type: string }[] } }).result;
assert.strictEqual(result.entries.length, 2);
assert.strictEqual(result.entries[0].name, 'src');
assert.strictEqual(result.entries[0].type, 'directory');
@@ -370,8 +370,8 @@ suite('ProtocolServerHandler', () => {
const transport = connectClient('client-browse-error');
transport.sent.length = 0;
const dirUri = URI.file('/missing');
sideEffects.browseErrors.set(dirUri.toString(), new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Directory not found: ${dirUri.toString()}`));
const dirUri = URI.file('/missing').toString();
sideEffects.browseErrors.set(dirUri, new ProtocolError(JSON_RPC_INTERNAL_ERROR, `Directory not found: ${dirUri}`));
const responsePromise = waitForResponse(transport, 2);
transport.simulateMessage(request(2, 'browseDirectory', { uri: dirUri }));
const resp = await responsePromise as { error?: { code: number; message: string } };

View File

@@ -9,12 +9,10 @@ import { fileURLToPath } from 'url';
import { WebSocket } from 'ws';
import { URI } from '../../../../base/common/uri.js';
import { PROTOCOL_VERSION } from '../../common/state/sessionCapabilities.js';
import { protocolReplacer, protocolReviver } from '../../common/state/jsonSerialization.js';
import {
isJsonRpcNotification,
isJsonRpcResponse,
JSON_RPC_PARSE_ERROR,
type IActionBroadcastParams,
type IFetchTurnsResult,
type IInitializeResult,
type IJsonRpcErrorResponse,
@@ -22,11 +20,11 @@ import {
type IListSessionsResult,
type INotificationBroadcastParams,
type IProtocolMessage,
type IProtocolNotification,
type IAhpNotification,
type IReconnectResult,
type IStateSnapshot,
} from '../../common/state/sessionProtocol.js';
import type { IDeltaAction, ISessionAddedNotification, ISessionRemovedNotification, IUsageAction } from '../../common/state/sessionActions.js';
import type { IActionEnvelope, IDeltaAction, ISessionAddedNotification, ISessionRemovedNotification, IUsageAction } from '../../common/state/sessionActions.js';
import type { ISessionState } from '../../common/state/sessionState.js';
// ---- JSON-RPC test client ---------------------------------------------------
@@ -40,8 +38,8 @@ class TestProtocolClient {
private readonly _ws: WebSocket;
private _nextId = 1;
private readonly _pendingCalls = new Map<number, IPendingCall>();
private readonly _notifications: IProtocolNotification[] = [];
private readonly _notifWaiters: { predicate: (n: IProtocolNotification) => boolean; resolve: (n: IProtocolNotification) => void; reject: (err: Error) => void }[] = [];
private readonly _notifications: IAhpNotification[] = [];
private readonly _notifWaiters: { predicate: (n: IAhpNotification) => boolean; resolve: (n: IAhpNotification) => void; reject: (err: Error) => void }[] = [];
constructor(port: number) {
this._ws = new WebSocket(`ws://127.0.0.1:${port}`);
@@ -52,7 +50,7 @@ class TestProtocolClient {
this._ws.on('open', () => {
this._ws.on('message', (data: Buffer | string) => {
const text = typeof data === 'string' ? data : data.toString('utf-8');
const msg = JSON.parse(text, protocolReviver);
const msg = JSON.parse(text);
this._handleMessage(msg);
});
resolve();
@@ -90,15 +88,13 @@ class TestProtocolClient {
/** Send a JSON-RPC notification (fire-and-forget). */
notify(method: string, params?: unknown): void {
const msg: IProtocolMessage = { jsonrpc: '2.0', method, params };
this._ws.send(JSON.stringify(msg, protocolReplacer));
this._ws.send(JSON.stringify({ jsonrpc: '2.0', method, params }));
}
/** Send a JSON-RPC request and await the response. */
call<T>(method: string, params?: unknown, timeoutMs = 5000): Promise<T> {
const id = this._nextId++;
const msg: IProtocolMessage = { jsonrpc: '2.0', id, method, params };
this._ws.send(JSON.stringify(msg, protocolReplacer));
this._ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
@@ -114,13 +110,13 @@ class TestProtocolClient {
}
/** Wait for a server notification matching a predicate. */
waitForNotification(predicate: (n: IProtocolNotification) => boolean, timeoutMs = 5000): Promise<IProtocolNotification> {
waitForNotification(predicate: (n: IAhpNotification) => boolean, timeoutMs = 5000): Promise<IAhpNotification> {
const existing = this._notifications.find(predicate);
if (existing) {
return Promise.resolve(existing);
}
return new Promise<IProtocolNotification>((resolve, reject) => {
return new Promise<IAhpNotification>((resolve, reject) => {
const timer = setTimeout(() => {
const idx = this._notifWaiters.findIndex(w => w.resolve === resolve);
if (idx >= 0) {
@@ -138,7 +134,7 @@ class TestProtocolClient {
}
/** Return all received notifications matching a predicate. */
receivedNotifications(predicate?: (n: IProtocolNotification) => boolean): IProtocolNotification[] {
receivedNotifications(predicate?: (n: IAhpNotification) => boolean): IAhpNotification[] {
return predicate ? this._notifications.filter(predicate) : [...this._notifications];
}
@@ -231,20 +227,20 @@ function nextSessionUri(): URI {
return URI.from({ scheme: 'mock', path: `/test-session-${++sessionCounter}` });
}
function isActionNotification(n: IProtocolNotification, actionType: string): boolean {
function isActionNotification(n: IAhpNotification, actionType: string): boolean {
if (n.method !== 'action') {
return false;
}
const params = n.params as IActionBroadcastParams;
return params.envelope.action.type === actionType;
const envelope = n.params as unknown as IActionEnvelope;
return envelope.action.type === actionType;
}
function getActionParams(n: IProtocolNotification): IActionBroadcastParams {
return n.params as IActionBroadcastParams;
function getActionEnvelope(n: IAhpNotification): IActionEnvelope {
return n.params as unknown as IActionEnvelope;
}
/** Perform handshake, create a session, subscribe, and return its URI. */
async function createAndSubscribeSession(c: TestProtocolClient, clientId: string): Promise<URI> {
async function createAndSubscribeSession(c: TestProtocolClient, clientId: string): Promise<string> {
await c.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId });
await c.call('createSession', { session: nextSessionUri(), provider: 'mock' });
@@ -260,7 +256,7 @@ async function createAndSubscribeSession(c: TestProtocolClient, clientId: string
return realSessionUri;
}
function dispatchTurnStarted(c: TestProtocolClient, session: URI, turnId: string, text: string, clientSeq: number): void {
function dispatchTurnStarted(c: TestProtocolClient, session: string, turnId: string, text: string, clientSeq: number): void {
c.notify('dispatchAction', {
clientSeq,
action: {
@@ -325,7 +321,7 @@ suite('Protocol WebSocket E2E', function () {
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'
);
const notification = (notif.params as INotificationBroadcastParams).notification as ISessionAddedNotification;
assert.strictEqual(notification.summary.resource.scheme, 'mock');
assert.strictEqual(URI.parse(notification.summary.resource).scheme, 'mock');
assert.strictEqual(notification.summary.provider, 'mock');
});
@@ -337,7 +333,7 @@ suite('Protocol WebSocket E2E', function () {
dispatchTurnStarted(client, sessionUri, 'turn-1', 'hello', 1);
const delta = await client.waitForNotification(n => isActionNotification(n, 'session/delta'));
const deltaAction = getActionParams(delta).envelope.action;
const deltaAction = getActionEnvelope(delta).action;
assert.strictEqual(deltaAction.type, 'session/delta');
if (deltaAction.type === 'session/delta') {
assert.strictEqual(deltaAction.content, 'Hello, world!');
@@ -356,7 +352,7 @@ suite('Protocol WebSocket E2E', function () {
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));
const toolComplete = await client.waitForNotification(n => isActionNotification(n, 'session/toolCallComplete'));
const tcAction = getActionParams(toolComplete).envelope.action;
const tcAction = getActionEnvelope(toolComplete).action;
if (tcAction.type === 'session/toolCallComplete') {
assert.strictEqual(tcAction.result.success, true);
}
@@ -372,7 +368,7 @@ suite('Protocol WebSocket E2E', function () {
dispatchTurnStarted(client, sessionUri, 'turn-err', 'error', 1);
const errorNotif = await client.waitForNotification(n => isActionNotification(n, 'session/error'));
const errorAction = getActionParams(errorNotif).envelope.action;
const errorAction = getActionEnvelope(errorNotif).action;
if (errorAction.type === 'session/error') {
assert.strictEqual(errorAction.error.message, 'Something went wrong');
}
@@ -399,7 +395,7 @@ suite('Protocol WebSocket E2E', function () {
});
const delta = await client.waitForNotification(n => isActionNotification(n, 'session/delta'));
const content = (getActionParams(delta).envelope.action as IDeltaAction).content;
const content = (getActionEnvelope(delta).action as IDeltaAction).content;
assert.strictEqual(content, 'Allowed.');
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
@@ -417,8 +413,8 @@ suite('Protocol WebSocket E2E', function () {
);
const result = await client.call<IListSessionsResult>('listSessions');
assert.ok(Array.isArray(result.sessions));
assert.ok(result.sessions.length >= 1, 'should have at least one session');
assert.ok(Array.isArray(result.items));
assert.ok(result.items.length >= 1, 'should have at least one session');
});
// 8. Reconnect
@@ -431,7 +427,7 @@ suite('Protocol WebSocket E2E', function () {
const allActions = client.receivedNotifications(n => n.method === 'action');
assert.ok(allActions.length > 0);
const missedFromSeq = getActionParams(allActions[0]).envelope.serverSeq - 1;
const missedFromSeq = getActionEnvelope(allActions[0]).serverSeq - 1;
client.close();
@@ -460,7 +456,7 @@ suite('Protocol WebSocket E2E', function () {
dispatchTurnStarted(client, sessionUri, 'turn-usage', 'with-usage', 1);
const usageNotif = await client.waitForNotification(n => isActionNotification(n, 'session/usage'));
const usageAction = getActionParams(usageNotif).envelope.action as IUsageAction;
const usageAction = getActionEnvelope(usageNotif).action as IUsageAction;
assert.strictEqual(usageAction.usage.inputTokens, 100);
assert.strictEqual(usageAction.usage.outputTokens, 50);
@@ -643,7 +639,7 @@ suite('Protocol WebSocket E2E', function () {
});
const modelChanged = await client.waitForNotification(n => isActionNotification(n, 'session/modelChanged'));
const action = getActionParams(modelChanged).envelope.action;
const action = getActionEnvelope(modelChanged).action;
assert.strictEqual(action.type, 'session/modelChanged');
if (action.type === 'session/modelChanged') {
assert.strictEqual((action as { model: string }).model, 'new-mock-model');