Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,17 @@ export interface IAgentReasoningEvent extends IAgentProgressEventBase {
readonly content: string;
}

/**
* The set of events returned by {@link IAgent.getSessionMessages} when
* reconstructing a session's history. Reasoning is carried inline on
* {@link IAgentMessageEvent.reasoningText} rather than as a separate event.
*/
export type SessionHistoryEvent =
| IAgentMessageEvent
| IAgentToolStartEvent
| IAgentToolCompleteEvent
| IAgentSubagentStartedEvent;

/** A steering message was consumed (sent to the model). */
export interface IAgentSteeringConsumedEvent extends IAgentProgressEventBase {
readonly type: 'steering_consumed';
Expand Down Expand Up @@ -452,7 +463,7 @@ export interface IAgent {
setPendingMessages?(session: URI, steeringMessage: PendingMessage | undefined, queuedMessages: readonly PendingMessage[]): void;

/** Retrieve all session events/messages for reconstruction. */
getSessionMessages(session: URI): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[]>;
getSessionMessages(session: URI): Promise<SessionHistoryEvent[]>;

/** Dispose a session, freeing resources. */
disposeSession(session: URI): Promise<void>;
Expand Down
37 changes: 28 additions & 9 deletions src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { FileSystemProviderErrorCode, IFileService, toFileSystemProviderErrorCod
import { InstantiationService } from '../../instantiation/common/instantiationService.js';
import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
import { ILogService } from '../../log/common/log.js';
import { AgentProvider, AgentSession, IAgent, IAgentCreateSessionConfig, IAgentMessageEvent, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent, AuthenticateParams, AuthenticateResult } from '../common/agentService.js';
import { AgentProvider, AgentSession, IAgent, IAgentCreateSessionConfig, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSubagentStartedEvent, IAgentToolStartEvent, SessionHistoryEvent, AuthenticateParams, AuthenticateResult } from '../common/agentService.js';
import { ISessionDataService } from '../common/sessionDataService.js';
import { ActionType, ActionEnvelope, INotification, RootAction, SessionAction, TerminalAction, isSessionAction } from '../common/state/sessionActions.js';
import type { CreateTerminalParams, ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../common/state/protocol/commands.js';
Expand Down Expand Up @@ -690,7 +690,7 @@ export class AgentService extends Disposable implements IAgentService {
* closes it.
*/
private _buildTurnsFromMessages(
messages: readonly (IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[],
messages: readonly SessionHistoryEvent[],
): Turn[] {
const turns: Turn[] = [];
// Track subagent metadata by parent tool call ID so we can inject
Expand Down Expand Up @@ -737,6 +737,16 @@ export class AgentService extends Disposable implements IAgentService {
currentTurn = startTurn(msg.messageId, '');
}

// Reasoning is bundled onto the assistant message and
// logically precedes its content/tool calls.
if (msg.reasoningText) {
currentTurn.responseParts.push({
kind: ResponsePartKind.Reasoning,
id: generateUuid(),
content: msg.reasoningText,
});
}

if (msg.content) {
currentTurn.responseParts.push({
kind: ResponsePartKind.Markdown,
Expand Down Expand Up @@ -821,7 +831,7 @@ export class AgentService extends Disposable implements IAgentService {
* tool calls.
*/
private _buildSubagentTurns(
parentMessages: readonly (IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[],
parentMessages: readonly SessionHistoryEvent[],
parentToolCallId: string,
childSessionUri: string,
): Turn[] {
Expand Down Expand Up @@ -903,12 +913,21 @@ export class AgentService extends Disposable implements IAgentService {
kind: ResponsePartKind.ToolCall,
toolCall: tc,
});
} else if (msg.type === 'message' && msg.role === 'assistant' && msg.content) {
responseParts.push({
kind: ResponsePartKind.Markdown,
id: generateUuid(),
content: msg.content,
});
} else if (msg.type === 'message' && msg.role === 'assistant') {
if (msg.reasoningText) {
responseParts.push({
kind: ResponsePartKind.Reasoning,
id: generateUuid(),
content: msg.reasoningText,
});
}
if (msg.content) {
responseParts.push({
kind: ResponsePartKind.Markdown,
id: generateUuid(),
content: msg.content,
});
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/vs/platform/agentHost/node/copilot/copilotAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { IFileService } from '../../../files/common/files.js';
import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
import { ILogService } from '../../../log/common/log.js';
import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js';
import { AgentSession, IAgent, IAgentAttachment, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentDeltaEvent, IAgentMessageEvent, IAgentModelInfo, IAgentProgressEvent, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSessionProjectInfo, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
import { AgentSession, IAgent, IAgentAttachment, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentDeltaEvent, IAgentMessageEvent, IAgentModelInfo, IAgentProgressEvent, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSessionProjectInfo, SessionHistoryEvent } from '../../common/agentService.js';
import { AutoApproveLevel, ISchemaProperty, createSchema, platformSessionSchema, schemaProperty } from '../../common/agentHostSchema.js';
import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
import { ISessionDataService, SESSION_DB_FILENAME } from '../../common/sessionDataService.js';
Expand Down Expand Up @@ -121,7 +121,7 @@ function buildWorktreeAnnouncementText(branchName: string): string {
) + '\n\n';
}

type AgentMessageOrEvent = IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent;
type AgentMessageOrEvent = SessionHistoryEvent;

/**
* Returns a copy of `messages` where `announcement` has been prepended to
Expand Down Expand Up @@ -727,7 +727,7 @@ export class CopilotAgent extends Disposable implements IAgent {
// No SDK-level enqueue is needed.
}

async getSessionMessages(session: URI): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[]> {
async getSessionMessages(session: URI): Promise<SessionHistoryEvent[]> {
const sessionId = AgentSession.id(session);
const entry = this._sessions.get(sessionId) ?? await this._resumeSession(sessionId).catch(err => {
this._logService.warn(`[Copilot:${sessionId}] Failed to resume session for message lookup`, err);
Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { INativeEnvironmentService } from '../../../environment/common/environme
import { IFileService } from '../../../files/common/files.js';
import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
import { ILogService } from '../../../log/common/log.js';
import { IAgentAttachment, IAgentMessageEvent, IAgentProgressEvent, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
import { IAgentAttachment, IAgentProgressEvent, SessionHistoryEvent } from '../../common/agentService.js';
import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
import { ISessionDatabase, ISessionDataService } from '../../common/sessionDataService.js';
import type { FileEdit, ToolDefinition } from '../../common/state/protocol/state.js';
Expand Down Expand Up @@ -330,7 +330,7 @@ export class CopilotAgentSession extends Disposable {
}
}

async getMessages(): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[]> {
async getMessages(): Promise<SessionHistoryEvent[]> {
const events = await this._wrapper.session.getMessages();
let db: ISessionDatabase | undefined;
try {
Expand Down
6 changes: 3 additions & 3 deletions src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { URI } from '../../../../base/common/uri.js';
import { IAgentMessageEvent, IAgentSubagentStartedEvent, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
import { SessionHistoryEvent } from '../../common/agentService.js';
import { stripRedundantCdPrefix } from '../../common/commandLineHelpers.js';
import { IFileEditRecord, ISessionDatabase } from '../../common/sessionDataService.js';
import { ToolResultContentType, type ToolResultContent } from '../../common/state/sessionState.js';
Expand Down Expand Up @@ -90,8 +90,8 @@ export async function mapSessionEvents(
db: ISessionDatabase | undefined,
events: readonly ISessionEvent[],
workingDirectory?: URI,
): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[]> {
const result: (IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[] = [];
): Promise<SessionHistoryEvent[]> {
const result: SessionHistoryEvent[] = [];
const toolInfoByCallId = new Map<string, { toolName: string; parameters: Record<string, unknown> | undefined; rewrittenArgs?: string }>();

// Collect all tool call IDs for edit tools so we can batch-query the database
Expand Down
34 changes: 34 additions & 0 deletions src/vs/platform/agentHost/test/node/agentService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,40 @@ suite('AgentService (node dispatcher)', () => {
assert.strictEqual(tc.confirmed, ToolCallConfirmationReason.NotNeeded);
});

test('interleaves reasoning, markdown, and tool calls in stream order on resume', async () => {
service.registerProvider(copilotAgent);
const { session } = await copilotAgent.createSession();
const sessions = await copilotAgent.listSessions();
const sessionResource = sessions[0].session;

copilotAgent.sessionMessages = [
{ type: 'message', session, role: 'user', messageId: 'u-1', content: 'Hello', toolRequests: [] },
{ type: 'message', session, role: 'assistant', messageId: 'a-1', content: 'Reply A', reasoningText: 'Thinking A', toolRequests: [{ toolCallId: 'tc-1', name: 'shell' }] },
{ type: 'tool_start', session, toolCallId: 'tc-1', toolName: 'shell', displayName: 'Shell', invocationMessage: 'Running...' },
{ type: 'tool_complete', session, toolCallId: 'tc-1', result: { success: true, pastTenseMessage: 'Ran', content: [{ type: ToolResultContentType.Text, text: 'ok' }] } },
{ type: 'message', session, role: 'assistant', messageId: 'a-2', content: 'Reply B', reasoningText: 'Thinking B', toolRequests: [] },
];

await service.restoreSession(sessionResource);

const state = service.stateManager.getSessionState(sessionResource.toString());
assert.ok(state);
const turn = state!.turns[0];
const summary = turn.responseParts.map(p => {
if (p.kind === ResponsePartKind.Reasoning) { return ['reasoning', p.content]; }
if (p.kind === ResponsePartKind.Markdown) { return ['markdown', p.content]; }
if (p.kind === ResponsePartKind.ToolCall) { return ['toolCall', p.toolCall.toolCallId]; }
return ['other'];
});
assert.deepStrictEqual(summary, [
['reasoning', 'Thinking A'],
['markdown', 'Reply A'],
['toolCall', 'tc-1'],
['reasoning', 'Thinking B'],
['markdown', 'Reply B'],
]);
});

test('flushes interrupted turns', async () => {
service.registerProvider(copilotAgent);
const { session } = await copilotAgent.createSession();
Expand Down
10 changes: 5 additions & 5 deletions src/vs/platform/agentHost/test/node/mockAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { observableValue } from '../../../../base/common/observable.js';
import type { IAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.js';
import { URI } from '../../../../base/common/uri.js';
import { type ISyncedCustomization } from '../../common/agentPluginManager.js';
import { AgentSession, type AgentProvider, type IAgent, type IAgentAttachment, type IAgentCreateSessionConfig, type IAgentCreateSessionResult, type IAgentDescriptor, type IAgentMessageEvent, type IAgentModelInfo, type IAgentProgressEvent, type IAgentResolveSessionConfigParams, type IAgentSessionConfigCompletionsParams, type IAgentSessionMetadata, type IAgentSubagentStartedEvent, type IAgentToolCompleteEvent, type IAgentToolStartEvent } from '../../common/agentService.js';
import { AgentSession, type AgentProvider, type IAgent, type IAgentAttachment, type IAgentCreateSessionConfig, type IAgentCreateSessionResult, type IAgentDescriptor, type IAgentModelInfo, type IAgentProgressEvent, type IAgentResolveSessionConfigParams, type IAgentSessionConfigCompletionsParams, type IAgentSessionMetadata, type SessionHistoryEvent } from '../../common/agentService.js';
import { ProtectedResourceMetadata, type ModelSelection } from '../../common/state/protocol/state.js';
import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
import { CustomizationStatus, ToolResultContentType, type CustomizationRef, type PendingMessage, type ToolCallResult } from '../../common/state/sessionState.js';
Expand Down Expand Up @@ -48,7 +48,7 @@ export class MockAgent implements IAgent {
customizations: CustomizationRef[] = [];

/** Configurable return value for getSessionMessages. */
sessionMessages: (IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[] = [];
sessionMessages: SessionHistoryEvent[] = [];

/** Optional overrides applied to session metadata from listSessions. */
sessionMetadataOverrides: Partial<Omit<IAgentSessionMetadata, 'session'>> = {};
Expand Down Expand Up @@ -100,7 +100,7 @@ export class MockAgent implements IAgent {
this.setPendingMessagesCalls.push({ session, steeringMessage, queuedMessages });
}

async getSessionMessages(_session: URI): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent | IAgentSubagentStartedEvent)[]> {
async getSessionMessages(_session: URI): Promise<SessionHistoryEvent[]> {
return this.sessionMessages;
}
Comment thread
roblourens marked this conversation as resolved.

Expand Down Expand Up @@ -190,7 +190,7 @@ export class ScriptedMockAgent implements IAgent {
* Message history for the pre-existing session: a single user→assistant
* turn with a tool call.
*/
private readonly _preExistingMessages: (IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent)[] = [
private readonly _preExistingMessages: SessionHistoryEvent[] = [
{ type: 'message', role: 'user', session: PRE_EXISTING_SESSION_URI, messageId: 'h-msg-1', content: 'What files are here?' },
{ type: 'tool_start', session: PRE_EXISTING_SESSION_URI, toolCallId: 'h-tc-1', toolName: 'list_files', displayName: 'List Files', invocationMessage: 'Listing files...' },
{ type: 'tool_complete', session: PRE_EXISTING_SESSION_URI, toolCallId: 'h-tc-1', result: { pastTenseMessage: 'Listed files', content: [{ type: ToolResultContentType.Text, text: 'file1.ts\nfile2.ts' }], success: true } satisfies ToolCallResult },
Expand Down Expand Up @@ -611,7 +611,7 @@ export class ScriptedMockAgent implements IAgent {
}
}

async getSessionMessages(session: URI): Promise<(IAgentMessageEvent | IAgentToolStartEvent | IAgentToolCompleteEvent)[]> {
async getSessionMessages(session: URI): Promise<SessionHistoryEvent[]> {
if (session.toString() === PRE_EXISTING_SESSION_URI.toString()) {
return this._preExistingMessages;
}
Expand Down
Loading