1
>
/*---------------------------------------------------------------------------------------------
sessionServerTools.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import { URI } from '../../../../base/common/uri.js';
7
>
import type { Mutable } from '../../../../base/common/types.js';
8
>
import { localize } from '../../../../nls.js';
9
>
import type { IAgentCreateSessionConfig, IAgentModelInfo, IAgentSessionMetadata } from '../../common/agentService.js';
10
>
import { SessionStatus } from '../../common/state/protocol/channels-session/state.js';
11
>
import { buildChatUri, buildDefaultChatUri, parseChatUri, readSessionGitState, readSessionGitHubState, ResponsePartKind, ToolCallStatus, TurnState, type Message, type ResponsePart, type ToolCallState, type ToolDefinition, type StringOrMarkdown, type Turn, type URI as ProtocolURI } from '../../common/state/sessionState.js';
12
>
import { buildOpenSessionLinkUri, CREATE_CHAT_TOOL_NAME, CREATE_SESSION_TOOL_NAME, parseOpenSessionLinkChatId, parseOpenSessionLinkUri, SEND_MESSAGE_TOOL_NAME } from '../../common/openSessionLink.js';
13
>
import { generateUuid } from '../../../../base/common/uuid.js';
14
>
import type { AgentHostStateManager } from '../agentHostStateManager.js';
15
>
import type { IServerToolDisplay, IServerToolDisplayResult, IServerToolGroup } from './agentServerToolHost.js';
16
>
17
>
export const listSessionsToolName = 'list_sessions';
18
>
export const getCurrentSessionToolName = 'get_current_session';
19
>
export const createSessionToolName = CREATE_SESSION_TOOL_NAME;
20
>
export const createChatToolName = CREATE_CHAT_TOOL_NAME;
21
>
export const sendMessageToolName = SEND_MESSAGE_TOOL_NAME;
22
>
export const getSessionContextToolName = 'get_session_context';
23
>
export const deleteSessionToolName = 'delete_session';
24
>
25
>
/**
26
>
* Maximum `create_session` recursion depth. A user/top-level session is depth 0;
27
>
* a session created by `create_session` from within a depth-N session is depth
28
>
* N+1. Once a session reaches this depth, its agent may not create further
29
>
* sessions — this bounds recursive spawn *chains* (A→B→C→…). Breadth is bounded
30
>
* separately by {@link maxCreatedSessions} plus the per-call user confirmation.
31
>
*/
32
>
const maxSessionSpawnDepth = 3;
33
>
34
>
/** Process-wide backstop against runaway spawning (breadth), independent of depth. */
35
>
const maxCreatedSessions = 25;
36
>
const maxCreatedChats = 25;
37
>
38
>
/** Process-wide backstop against runaway `send_message` fan-out. */
39
>
const maxSentMessages = 50;
40
>
41
>
const sessionConfirmationToolNames: ReadonlySet<string> = new Set([createSessionToolName, createChatToolName, sendMessageToolName, deleteSessionToolName]);
42
>
43
>
/** Whether the given session server tool requires user confirmation before it runs. */
44
>
export function sessionToolRequiresConfirmation(toolName: string): boolean {
45
return sessionConfirmationToolNames.has(toolName);
46
}
48
>
const listSessionsStatusValues = ['idle', 'inProgress', 'inputNeeded', 'error', 'archived'] as const;
49
>
50
>
const listSessionsInputSchema: ToolDefinition['inputSchema'] = {
51
>
type: 'object',
52
>
properties: {
53
>
session: { type: 'string', description: 'Return only the session with this URI or `agent-host-session://` link (a direct lookup that ignores the other filters). Use this to fetch one known session\'s metadata.' },
54
>
status: {
55
>
type: 'array',
56
>
items: { type: 'string', enum: [...listSessionsStatusValues] },
57
>
description: 'Only return sessions whose status matches one of these (e.g. `inputNeeded` for sessions awaiting a reply, `inProgress` for running ones, `archived` for sessions marked Done/completed — implies `includeArchived`). Omit to return every status.',
58
>
},
59
>
workspace: { type: 'string', description: 'Only return sessions whose working directory is this folder — an absolute path or a workspace URI.' },
60
>
withChanges: { type: 'boolean', description: 'When true, only return sessions that have pending worktree changes.' },
61
>
unread: { type: 'boolean', description: 'When true, only return sessions with updates the user has not seen yet.' },
62
>
withPullRequest: { type: 'boolean', description: 'When true, only return sessions that have a linked GitHub pull request.' },
63
>
includeArchived: { type: 'boolean', description: 'Whether to include archived sessions. Defaults to false; set true to also return archived sessions.' },
64
>
createdAfter: { type: 'string', description: 'Only return sessions created at or after this time (ISO-8601 timestamp, e.g. `2025-01-31T00:00:00Z`).' },
65
>
createdBefore: { type: 'string', description: 'Only return sessions created at or before this time (ISO-8601 timestamp).' },
66
>
},
67
>
};
68
>
69
>
const createSessionInputSchema: ToolDefinition['inputSchema'] = {
70
>
type: 'object',
71
>
properties: {
72
>
workspace: { type: 'string', description: 'Absolute folder path, workspace URI, or a working directory from an existing session.' },
73
>
prompt: { type: 'string', description: 'Initial prompt to send to the new session.' },
74
>
model: { type: 'string', description: 'Optional model ID or display name.' },
75
>
},
76
>
required: ['workspace', 'prompt'],
77
>
};
78
>
79
>
const getCurrentSessionInputSchema: ToolDefinition['inputSchema'] = {
80
>
type: 'object',
81
>
properties: {},
82
>
};
83
>
84
>
const createChatInputSchema: ToolDefinition['inputSchema'] = {
85
>
type: 'object',
86
>
properties: {
87
>
session: { type: 'string', description: 'Optional session to add the chat to: a session URI from `list_sessions` or an `agent-host-session://` link. Defaults to the current session when omitted.' },
88
>
prompt: { type: 'string', description: 'Initial prompt to send to the new chat.' },
89
>
title: { type: 'string', description: 'Optional title for the new chat.' },
90
>
model: { type: 'string', description: 'Optional model ID or display name. Defaults to the session\'s model.' },
91
>
},
92
>
required: ['prompt'],
93
>
};
94
>
95
>
const deleteSessionInputSchema: ToolDefinition['inputSchema'] = {
96
>
type: 'object',
97
>
properties: {
98
>
session: { type: 'string', description: 'The session to delete: a session URI from `list_sessions` or an `agent-host-session://` link (e.g. from `create_session`).' },
99
>
},
100
>
required: ['session'],
101
>
};
102
>
103
>
const sendMessageInputSchema: ToolDefinition['inputSchema'] = {
104
>
type: 'object',
105
>
properties: {
106
>
session: { type: 'string', description: 'The session or chat to message: a session URI from `list_sessions`, or an `agent-host-session://` link (from `create_session`/`create_chat`; a `create_chat` link targets that specific chat).' },
107
>
message: { type: 'string', description: 'The message to send.' },
108
>
},
109
>
required: ['session', 'message'],
110
>
};
111
>
112
>
const sessionContextDetailValues = ['summary', 'digest', 'full'] as const;
113
>
114
>
const getSessionContextInputSchema: ToolDefinition['inputSchema'] = {
115
>
type: 'object',
116
>
properties: {
117
>
session: { type: 'string', description: 'The session or chat to read: a session URI from `list_sessions`, or an `agent-host-session://` link (a `create_chat` link targets that specific chat).' },
118
>
detail: {
119
>
type: 'string',
120
>
enum: [...sessionContextDetailValues],
121
>
description: 'How much conversation detail to return. `summary` (default): status and a short per-turn gist (the message plus a compact snippet of the reply). `digest`: adds the full assistant reply text and tool-call names. `full`: adds tool-call inputs. Higher levels return more tokens.',
122
>
},
123
>
transcriptLimit: { type: 'number', description: 'Maximum number of most-recent turns to include. Defaults to 10; capped at 50.' },
124
>
},
125
>
required: ['session'],
126
>
};
127
>
128
>
/** Protocol tool definitions for the session-management server tools. */
129
>
export const sessionServerToolDefinitions: ToolDefinition[] = [
130
>
{
131
>
name: listSessionsToolName,
132
>
title: 'List Sessions',
133
>
description: 'List sessions and their compact metadata (status, activity, working directory, project, worktree changes, git/GitHub info, timestamps). Pass `session` to fetch a single known session by URI. By default archived sessions are omitted. Optionally filter by `status`, `workspace`, `withChanges`, `unread`, `withPullRequest`, `includeArchived`, `createdAfter`, or `createdBefore`.',
134
>
inputSchema: listSessionsInputSchema,
135
>
annotations: { readOnlyHint: true },
136
>
},
137
>
{
138
>
name: getCurrentSessionToolName,
139
>
title: 'Get Current Session',
140
>
description: 'Get metadata and the open link for the session this conversation is running in. Use this to reference the current session (for example before adding a chat to it).',
141
>
inputSchema: getCurrentSessionInputSchema,
142
>
annotations: { readOnlyHint: true },
143
>
},
144
>
{
145
>
name: createSessionToolName,
146
>
title: 'Create Session',
147
>
description: 'Create a session in a workspace and start it with an initial prompt. The UI shows a "Session Created" confirmation with a button to open it, so reply with a single short sentence confirming the session was created and do NOT print the session URL or tell the user to click a button.',
148
>
inputSchema: createSessionInputSchema,
149
>
annotations: { readOnlyHint: false },
150
>
},
151
>
{
152
>
name: createChatToolName,
153
>
title: 'Create Chat',
154
>
description: 'Add a new chat to an existing session and start it with an initial prompt. Omit `session` to add the chat to the current session; otherwise pass a session URI from `list_sessions`. Optionally pass a `model` to use for the chat (defaults to the session\'s model). The UI shows a "Chat Created" confirmation with a button to open the session, so reply with a single short sentence and do NOT print the session URL or tell the user to click a button.',
155
>
inputSchema: createChatInputSchema,
156
>
annotations: { readOnlyHint: false },
157
>
},
158
>
{
159
>
name: sendMessageToolName,
160
>
title: 'Send Message',
161
>
description: 'Send a message to an existing session or chat, starting a new turn there. Provide a session URI from `list_sessions` or an `agent-host-session://` link (a `create_chat` link targets that specific chat). The message is delivered asynchronously — this tool does not wait for or return the reply. The UI shows a confirmation with a button to open the target, so reply with a single short sentence and do NOT print the URL or tell the user to click a button.',
162
>
inputSchema: sendMessageInputSchema,
163
>
annotations: { readOnlyHint: false },
164
>
},
165
>
{
166
>
name: getSessionContextToolName,
167
>
title: 'Get Session Context',
168
>
description: 'Read the recent conversation of an existing session or chat: a compacted transcript of its turns (messages, replies, and tool calls). Use this to see what a session you created is doing, or to gather context before sending it a message. Returns a compacted summary by default (`detail: "summary"`); request `digest` or `full` for more detail. For session metadata (status, working directory, changes, …) use `list_sessions` with the `session` argument.',
169
>
inputSchema: getSessionContextInputSchema,
170
>
annotations: { readOnlyHint: true },
171
>
},
172
>
{
173
>
name: deleteSessionToolName,
174
>
title: 'Delete Session',
175
>
description: 'Permanently delete a session (identified by a session URI from `list_sessions`), including its stored data. This cannot be undone. Refuses to delete the current session.',
176
>
inputSchema: deleteSessionInputSchema,
177
>
annotations: { readOnlyHint: false, destructiveHint: true },
178
>
},
179
>
];
180
>
181
>
/** Resolves the owning backend session URI for the channel a tool call runs on. */
182
>
export function currentSessionUri(toolCallChannel: ProtocolURI): URI {
183
const owning = parseChatUri(toolCallChannel) ?? undefined;
184
return URI.parse(owning?.session ?? toolCallChannel);
185
}
187
>
interface ICreateSessionArgs {
188
>
readonly workspace?: unknown;
189
>
readonly prompt?: unknown;
190
>
readonly model?: unknown;
191
>
}
192
>
193
>
export interface IResolvedCreateSessionArgs {
194
>
readonly workspace: URI;
195
>
readonly prompt: string;
196
>
readonly model?: IAgentModelInfo;
197
>
}
198
>
199
>
/** Minimal dependency surface needed by the session server-tool group. */
200
>
export interface ISessionServerToolAccessor {
201
>
readonly listSessions: () => Promise<readonly IAgentSessionMetadata[]>;
202
>
readonly createSession: (config: IAgentCreateSessionConfig) => Promise<URI>;
203
>
readonly getModels: () => readonly IAgentModelInfo[];
204
>
readonly startPrompt: (session: URI, chat: URI, prompt: string) => Promise<void>;
205
>
readonly createChat: (session: URI, chat: URI, options?: { title?: string; model?: IAgentModelInfo }) => Promise<void>;
206
>
readonly deleteSession: (session: URI) => Promise<void>;
207
>
/** Reads a point-in-time snapshot of a session's chat conversation (default chat, or a specific chat by id). */
208
>
readonly getChatContext: (session: URI, chatId?: string) => IChatContextSnapshot | undefined;
209
>
/** The spawn depth of a session (0 for a user/top-level session, N for one created N levels deep by `create_session`). */
210
>
readonly getSessionSpawnDepth: (session: URI) => number;
211
>
/** Records the spawn depth of a freshly-created session so its own `create_session` calls can enforce the recursion limit. */
212
>
readonly setSessionSpawnDepth: (session: URI, depth: number) => void;
213
>
}
214
>
215
>
/** Point-in-time snapshot of a chat's conversation, read from the host state. */
216
>
export interface IChatContextSnapshot {
217
>
/** Completed turns, oldest first. */
218
>
readonly turns: readonly Turn[];
219
>
/** The in-progress turn, if the chat is mid-response. */
220
>
readonly activeTurn?: Pick<Turn, 'message' | 'responseParts'>;
221
>
/** `true` when older completed turns exist beyond the in-memory window. */
222
>
readonly hasMoreHistory: boolean;
223
>
}
224
>
225
>
interface ISerializedGitState {
226
>
readonly branch?: string;
227
>
readonly baseBranch?: string;
228
>
readonly upstreamBranch?: string;
229
>
readonly ahead?: number;
230
>
readonly behind?: number;
231
>
readonly uncommittedChanges?: number;
232
>
}
233
>
234
>
interface ISerializedGitHubState {
235
>
readonly owner?: string;
236
>
readonly repo?: string;
237
>
readonly pullRequestUrl?: string;
238
>
}
239
>
240
>
interface ISerializedSession {
241
>
readonly session: string;
242
>
readonly title?: string;
243
>
readonly status?: string;
244
>
/** Human-readable description of what the session is currently doing. */
245
>
readonly activity?: string;
246
>
readonly workingDirectory?: string;
247
>
/** Display name of the session's project/workspace. */
248
>
readonly project?: string;
249
>
/** `true` when the session has updates the user has not yet seen. */
250
>
readonly unread?: boolean;
251
>
/** ISO-8601 timestamp of when the session was created. */
252
>
readonly createdAt?: string;
253
>
/** ISO-8601 timestamp of the session's last activity. */
254
>
readonly modifiedAt?: string;
255
>
readonly changes?: IAgentSessionMetadata['changes'];
256
>
readonly changesets?: readonly {
257
>
readonly label: string;
258
>
readonly changeKind: string;
259
>
readonly uriTemplate: string;
260
>
readonly description?: string;
261
>
}[];
262
>
readonly git?: ISerializedGitState;
263
>
readonly github?: ISerializedGitHubState;
264
>
}
265
>
266
function getRequiredString(value: unknown, field: string, toolName: string): string {
267
if (typeof value !== 'string' || value.length === 0) {