mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-29 13:03:42 +01:00
agentHost: ui side for queued messages (#304954)
* agentHost: initial queuing/steering data flows * agentHost: ui side for queued messages Steering messages still don't quite work yet, I need to hook up some more stuff in the CLI for that I believe. * comments * rm dead code
This commit is contained in:
@@ -16,7 +16,7 @@ import { NullLogService } from '../../../log/common/log.js';
|
||||
import { AgentSession, IAgent } from '../../common/agentService.js';
|
||||
import { ISessionDataService } from '../../common/sessionDataService.js';
|
||||
import { ActionType, IActionEnvelope, ISessionAction } from '../../common/state/sessionActions.js';
|
||||
import { ResponsePartKind, SessionLifecycle, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, type IMarkdownResponsePart, type IToolCallCompletedState, type IToolCallResponsePart } from '../../common/state/sessionState.js';
|
||||
import { PendingMessageKind, ResponsePartKind, SessionLifecycle, SessionStatus, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, type IMarkdownResponsePart, type IToolCallCompletedState, type IToolCallResponsePart } from '../../common/state/sessionState.js';
|
||||
import { AgentSideEffects } from '../../node/agentSideEffects.js';
|
||||
import { SessionStateManager } from '../../node/sessionStateManager.js';
|
||||
import { MockAgent } from './mockAgent.js';
|
||||
@@ -522,4 +522,204 @@ suite('AgentSideEffects', () => {
|
||||
assert.deepStrictEqual(result, { authenticated: false });
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Pending message sync -----------------------------------------------
|
||||
|
||||
suite('pending message sync', () => {
|
||||
|
||||
test('syncs steering message to agent on SessionPendingMessageSet', () => {
|
||||
setupSession();
|
||||
|
||||
const action = {
|
||||
type: ActionType.SessionPendingMessageSet as const,
|
||||
session: sessionUri.toString(),
|
||||
kind: PendingMessageKind.Steering,
|
||||
id: 'steer-1',
|
||||
userMessage: { text: 'focus on tests' },
|
||||
};
|
||||
stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 });
|
||||
sideEffects.handleAction(action);
|
||||
|
||||
assert.strictEqual(agent.setPendingMessagesCalls.length, 1);
|
||||
assert.deepStrictEqual(agent.setPendingMessagesCalls[0].steeringMessage, { id: 'steer-1', userMessage: { text: 'focus on tests' } });
|
||||
assert.deepStrictEqual(agent.setPendingMessagesCalls[0].queuedMessages, []);
|
||||
});
|
||||
|
||||
test('syncs queued message to agent on SessionPendingMessageSet', () => {
|
||||
setupSession();
|
||||
|
||||
const action = {
|
||||
type: ActionType.SessionPendingMessageSet as const,
|
||||
session: sessionUri.toString(),
|
||||
kind: PendingMessageKind.Queued,
|
||||
id: 'q-1',
|
||||
userMessage: { text: 'queued message' },
|
||||
};
|
||||
stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 });
|
||||
sideEffects.handleAction(action);
|
||||
|
||||
// Queued messages are not forwarded to the agent; the server controls consumption
|
||||
assert.strictEqual(agent.setPendingMessagesCalls.length, 1);
|
||||
assert.strictEqual(agent.setPendingMessagesCalls[0].steeringMessage, undefined);
|
||||
assert.deepStrictEqual(agent.setPendingMessagesCalls[0].queuedMessages, []);
|
||||
|
||||
// Session was idle, so the queued message is consumed immediately
|
||||
assert.strictEqual(agent.sendMessageCalls.length, 1);
|
||||
assert.strictEqual(agent.sendMessageCalls[0].prompt, 'queued message');
|
||||
});
|
||||
|
||||
test('syncs on SessionPendingMessageRemoved', () => {
|
||||
setupSession();
|
||||
|
||||
// Add a queued message
|
||||
const setAction = {
|
||||
type: ActionType.SessionPendingMessageSet as const,
|
||||
session: sessionUri.toString(),
|
||||
kind: PendingMessageKind.Queued,
|
||||
id: 'q-rm',
|
||||
userMessage: { text: 'will be removed' },
|
||||
};
|
||||
stateManager.dispatchClientAction(setAction, { clientId: 'test', clientSeq: 1 });
|
||||
sideEffects.handleAction(setAction);
|
||||
|
||||
agent.setPendingMessagesCalls.length = 0;
|
||||
|
||||
// Remove
|
||||
const removeAction = {
|
||||
type: ActionType.SessionPendingMessageRemoved as const,
|
||||
session: sessionUri.toString(),
|
||||
kind: PendingMessageKind.Queued,
|
||||
id: 'q-rm',
|
||||
};
|
||||
stateManager.dispatchClientAction(removeAction, { clientId: 'test', clientSeq: 2 });
|
||||
sideEffects.handleAction(removeAction);
|
||||
|
||||
assert.strictEqual(agent.setPendingMessagesCalls.length, 1);
|
||||
assert.deepStrictEqual(agent.setPendingMessagesCalls[0].queuedMessages, []);
|
||||
});
|
||||
|
||||
test('syncs on SessionQueuedMessagesReordered', () => {
|
||||
setupSession();
|
||||
|
||||
// Add two queued messages
|
||||
const setA = { type: ActionType.SessionPendingMessageSet as const, session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-a', userMessage: { text: 'A' } };
|
||||
stateManager.dispatchClientAction(setA, { clientId: 'test', clientSeq: 1 });
|
||||
sideEffects.handleAction(setA);
|
||||
|
||||
const setB = { type: ActionType.SessionPendingMessageSet as const, session: sessionUri.toString(), kind: PendingMessageKind.Queued, id: 'q-b', userMessage: { text: 'B' } };
|
||||
stateManager.dispatchClientAction(setB, { clientId: 'test', clientSeq: 2 });
|
||||
sideEffects.handleAction(setB);
|
||||
|
||||
agent.setPendingMessagesCalls.length = 0;
|
||||
|
||||
// Reorder
|
||||
const reorderAction = { type: ActionType.SessionQueuedMessagesReordered as const, session: sessionUri.toString(), order: ['q-b', 'q-a'] };
|
||||
stateManager.dispatchClientAction(reorderAction, { clientId: 'test', clientSeq: 3 });
|
||||
sideEffects.handleAction(reorderAction);
|
||||
|
||||
assert.strictEqual(agent.setPendingMessagesCalls.length, 1);
|
||||
assert.deepStrictEqual(agent.setPendingMessagesCalls[0].queuedMessages, []);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Queued message consumption -----------------------------------------
|
||||
|
||||
suite('queued message consumption', () => {
|
||||
|
||||
test('auto-starts turn from queued message on idle', () => {
|
||||
setupSession();
|
||||
disposables.add(sideEffects.registerProgressListener(agent));
|
||||
|
||||
// Queue a message while a turn is active
|
||||
startTurn('turn-1');
|
||||
const setAction = {
|
||||
type: ActionType.SessionPendingMessageSet as const,
|
||||
session: sessionUri.toString(),
|
||||
kind: PendingMessageKind.Queued,
|
||||
id: 'q-auto',
|
||||
userMessage: { text: 'auto queued' },
|
||||
};
|
||||
stateManager.dispatchClientAction(setAction, { clientId: 'test', clientSeq: 1 });
|
||||
sideEffects.handleAction(setAction);
|
||||
|
||||
// Message should NOT be consumed yet (turn is active)
|
||||
assert.strictEqual(agent.sendMessageCalls.length, 0);
|
||||
|
||||
const envelopes: IActionEnvelope[] = [];
|
||||
disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e)));
|
||||
|
||||
// Fire idle → turn completes → queued message should be consumed
|
||||
agent.fireProgress({ session: sessionUri, type: 'idle' });
|
||||
|
||||
const turnComplete = envelopes.find(e => e.action.type === ActionType.SessionTurnComplete);
|
||||
assert.ok(turnComplete, 'should dispatch session/turnComplete');
|
||||
|
||||
const turnStarted = envelopes.find(e => e.action.type === ActionType.SessionTurnStarted);
|
||||
assert.ok(turnStarted, 'should dispatch session/turnStarted for queued message');
|
||||
assert.strictEqual((turnStarted!.action as { queuedMessageId?: string }).queuedMessageId, 'q-auto');
|
||||
|
||||
assert.strictEqual(agent.sendMessageCalls.length, 1);
|
||||
assert.strictEqual(agent.sendMessageCalls[0].prompt, 'auto queued');
|
||||
|
||||
// Queued message should be removed from state
|
||||
const state = stateManager.getSessionState(sessionUri.toString());
|
||||
assert.strictEqual(state?.queuedMessages, undefined);
|
||||
});
|
||||
|
||||
test('does not consume queued message while a turn is active', () => {
|
||||
setupSession();
|
||||
startTurn('turn-1');
|
||||
|
||||
const envelopes: IActionEnvelope[] = [];
|
||||
disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e)));
|
||||
|
||||
const setAction = {
|
||||
type: ActionType.SessionPendingMessageSet as const,
|
||||
session: sessionUri.toString(),
|
||||
kind: PendingMessageKind.Queued,
|
||||
id: 'q-wait',
|
||||
userMessage: { text: 'should wait' },
|
||||
};
|
||||
stateManager.dispatchClientAction(setAction, { clientId: 'test', clientSeq: 1 });
|
||||
sideEffects.handleAction(setAction);
|
||||
|
||||
// No turn started for the queued message
|
||||
const turnStarted = envelopes.find(e => e.action.type === ActionType.SessionTurnStarted);
|
||||
assert.strictEqual(turnStarted, undefined, 'should not start a turn while one is active');
|
||||
assert.strictEqual(agent.sendMessageCalls.length, 0);
|
||||
|
||||
// Queued message still in state
|
||||
const state = stateManager.getSessionState(sessionUri.toString());
|
||||
assert.strictEqual(state?.queuedMessages?.length, 1);
|
||||
assert.strictEqual(state?.queuedMessages?.[0].id, 'q-wait');
|
||||
});
|
||||
|
||||
test('dispatches SessionPendingMessageRemoved for steering messages', () => {
|
||||
setupSession();
|
||||
|
||||
const envelopes: IActionEnvelope[] = [];
|
||||
disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e)));
|
||||
|
||||
const action = {
|
||||
type: ActionType.SessionPendingMessageSet as const,
|
||||
session: sessionUri.toString(),
|
||||
kind: PendingMessageKind.Steering,
|
||||
id: 'steer-rm',
|
||||
userMessage: { text: 'steer me' },
|
||||
};
|
||||
stateManager.dispatchClientAction(action, { clientId: 'test', clientSeq: 1 });
|
||||
sideEffects.handleAction(action);
|
||||
|
||||
const removal = envelopes.find(e =>
|
||||
e.action.type === ActionType.SessionPendingMessageRemoved &&
|
||||
(e.action as { kind: PendingMessageKind }).kind === PendingMessageKind.Steering
|
||||
);
|
||||
assert.ok(removal, 'should dispatch SessionPendingMessageRemoved for steering');
|
||||
assert.strictEqual((removal!.action as { id: string }).id, 'steer-rm');
|
||||
|
||||
// Steering message should be removed from state
|
||||
const state = stateManager.getSessionState(sessionUri.toString());
|
||||
assert.strictEqual(state?.steeringMessage, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Emitter } from '../../../../base/common/event.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import type { IAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.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 { ToolResultContentType, type IToolCallResult } from '../../common/state/sessionState.js';
|
||||
import { ToolResultContentType, type IPendingMessage, type IToolCallResult } from '../../common/state/sessionState.js';
|
||||
|
||||
/**
|
||||
* General-purpose mock agent for unit tests. Tracks all method calls
|
||||
@@ -22,6 +22,7 @@ export class MockAgent implements IAgent {
|
||||
|
||||
|
||||
readonly sendMessageCalls: { session: URI; prompt: string }[] = [];
|
||||
readonly setPendingMessagesCalls: { session: URI; steeringMessage: IPendingMessage | undefined; queuedMessages: readonly IPendingMessage[] }[] = [];
|
||||
readonly disposeSessionCalls: URI[] = [];
|
||||
readonly abortSessionCalls: URI[] = [];
|
||||
readonly respondToPermissionCalls: { requestId: string; approved: boolean }[] = [];
|
||||
@@ -66,6 +67,10 @@ export class MockAgent implements IAgent {
|
||||
this.sendMessageCalls.push({ session, prompt });
|
||||
}
|
||||
|
||||
setPendingMessages(session: URI, steeringMessage: IPendingMessage | undefined, queuedMessages: readonly IPendingMessage[]): void {
|
||||
this.setPendingMessagesCalls.push({ session, steeringMessage, queuedMessages });
|
||||
}
|
||||
|
||||
async getSessionMessages(_session: URI): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent)[]> {
|
||||
return this.sessionMessages;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user