Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/common/state/sessionState.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
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
// Immutable state types for the sessions process protocol.
7
// See protocol.md for the full design rationale.
8
//
9
// Most types are imported from the auto-generated protocol layer
10
// (synced from the agent-host-protocol repo). This file adds VS Code-specific
11
// helpers and re-exports.
12
13
import { hasKey } from '../../../../base/common/types.js';
14
import {
15
SessionLifecycle,
16
ToolResultContentType,
17
ToolResultFileEditContent,
18
type ActiveTurn,
19
type RootState,
20
type SessionState,
21
type SessionSummary,
22
type ToolCallCancelledState,
23
type ToolCallCompletedState,
24
type ToolCallResult,
25
type ToolCallState,
26
type ToolResultContent,
27
type ToolResultSubagentContent,
28
type ToolResultTextContent,
29
type UserMessage,
30
TerminalState,
31
} from './protocol/state.js';
32
33
// Re-export everything from the protocol state module
34
export {
35
type ActiveTurn,
36
type AgentInfo,
37
type ConfigPropertySchema,
38
type ConfigSchema,
39
type ContentRef,
40
type ErrorInfo,
41
type ProjectInfo,
42
type MarkdownResponsePart,
43
type MessageAttachment,
44
type ReasoningResponsePart,
45
type ResponsePart,
46
type RootState,
47
type SessionActiveClient,
48
type SessionConfigState,
49
type FileEdit as ISessionFileDiff,
50
type ModelSelection,
51
type SessionModelInfo,
52
type SessionState,
53
type SessionSummary,
54
type Snapshot,
55
type TerminalState,
56
type ToolAnnotations,
57
type ToolCallCancelledState,
58
type ToolCallCompletedState,
59
type ToolCallPendingConfirmationState,
60
type ToolCallPendingResultConfirmationState,
61
type ToolCallResponsePart,
62
type ToolCallResult,
63
type ToolCallRunningState,
64
type ToolCallState,
65
type ToolCallStreamingState,
66
type ToolDefinition,
67
type CustomizationRef,
68
type SessionCustomization,
69
type ToolResultEmbeddedResourceContent as IToolResultBinaryContent,
70
type ToolResultContent,
71
type ToolResultFileEditContent,
72
type ToolResultSubagentContent,
73
type ToolResultTextContent,
74
type Turn,
75
type UsageInfo,
76
type UserMessage,
77
type PendingMessage,
78
type StringOrMarkdown,
79
type URI,
80
type SessionInputRequest,
81
type SessionInputQuestion,
82
type SessionInputAnswer,
83
type SessionInputOption,
84
AttachmentType,
85
CustomizationStatus,
86
PendingMessageKind,
87
PolicyState,
88
ResponsePartKind,
89
SessionInputAnswerState,
90
SessionInputAnswerValueKind,
91
SessionInputQuestionKind,
92
SessionInputResponseKind,
93
SessionLifecycle,
94
SessionStatus,
95
ToolCallConfirmationReason,
96
ToolCallCancellationReason,
97
ToolCallStatus,
98
ToolResultContentType,
99
TurnState,
100
} from './protocol/state.js';
101
102
// ---- File edit kind ---------------------------------------------------------
103
104
/**
105
* The kind of file edit operation. Derived from the presence/absence of
106
* `before`/`after` in {@link ToolResultFileEditContent}.
107
*/
108
export const enum FileEditKind {
109
/** Content edit (same file URI, different content). */
110
Edit = 'edit',
111
/** File creation (no before state). */
112
Create = 'create',
113
/** File deletion (no after state). */
114
Delete = 'delete',
115
/** File rename/move (different before and after URIs). */
116
Rename = 'rename',
117
}
118
119
// ---- Well-known URIs --------------------------------------------------------
120
121
/** URI for the root state subscription. */
122
export const ROOT_STATE_URI = 'agenthost:/root';
123
124
// ---- VS Code-specific derived types -----------------------------------------
125
126
/**
127
* A tool call in a terminal state, stored in completed turns.
128
*/
129
export type ICompletedToolCall = ToolCallCompletedState | ToolCallCancelledState;
130
131
/**
132
* Derived status type for the tool call lifecycle.
133
*/
134
export type ToolCallStatusString = ToolCallState['status'];
135
136
// ---- Tool output helper -----------------------------------------------------
137
138
/**
139
* Extracts a plain-text tool output string from a tool call result's `content`
140
* array. Joins all text-type content parts into a single string.
141
*
142
* Returns `undefined` if there are no text content parts.
143
*/
144
export function getToolOutputText(result: ToolCallResult): string | undefined {
145
if (!result.content || result.content.length === 0) {
146
return undefined;
147
}
148
const textParts: ToolResultTextContent[] = [];
149
for (const c of result.content) {
150
if (hasKey(c, { type: true }) && c.type === ToolResultContentType.Text) {
151
textParts.push(c);
152
}
153
}
154
if (textParts.length === 0) {
155
return undefined;
156
}
157
return textParts.map(p => p.text).join('\n');
158
}
159
160
/**
161
* Extracts file edit content entries from a tool call result's `content` array.
162
* Returns an empty array if there are no file edit content parts.
163
*/
164
export function getToolFileEdits(result: ToolCallResult): ToolResultFileEditContent[] {
165
if (!result.content || result.content.length === 0) {
166
return [];
167
}
168
const edits: ToolResultFileEditContent[] = [];
169
for (const c of result.content) {
170
if (hasKey(c, { type: true }) && c.type === ToolResultContentType.FileEdit) {
171
edits.push(c);
172
}
173
}
174
return edits;
175
}
176
177
/**
178
* Extracts the first subagent content entry from a tool call's `content` array.
179
* Works with both completed tool call results and running tool call states.
180
* Returns `undefined` if there are no subagent content parts.
181
*/
182
export function getToolSubagentContent(result: { content?: readonly ToolResultContent[] }): ToolResultSubagentContent | undefined {
183
if (!result.content || result.content.length === 0) {
184
return undefined;
185
}
186
for (const c of result.content) {
187
if (hasKey(c, { type: true }) && c.type === ToolResultContentType.Subagent) {
188
return c as ToolResultSubagentContent;
189
}
190
}
191
return undefined;
192
}
193
194
// ---- Subagent URI helpers ---------------------------------------------------
195
196
/**
197
* Builds a subagent session URI from a parent session URI and tool call ID.
198
* Convention: `{parentSessionUri}/subagent/{toolCallId}`
199
*/
200
export function buildSubagentSessionUri(parentSession: string, toolCallId: string): string {
201
// Normalize: strip trailing slash from parent to avoid double-slash in URI
202
const parent = parentSession.endsWith('/') ? parentSession.slice(0, -1) : parentSession;
203
return `${parent}/subagent/${toolCallId}`;
204
}
205
206
/**
207
* Parses a subagent session URI into its parent session URI and tool call ID.
208
* Returns `undefined` if the URI does not follow the subagent convention.
209
*/
210
export function parseSubagentSessionUri(uri: string): { parentSession: string; toolCallId: string } | undefined {
211
const idx = uri.lastIndexOf('/subagent/');
212
if (idx < 0) {
213
return undefined;
214
}
215
const toolCallId = uri.substring(idx + '/subagent/'.length);
216
if (!toolCallId) {
217
return undefined;
218
}
219
return {
220
parentSession: uri.substring(0, idx),
221
toolCallId,
222
};
223
}
224
225
/**
226
* Returns whether a session URI represents a subagent session.
227
*/
228
export function isSubagentSession(uri: string): boolean {
229
return uri.includes('/subagent/');
230
}
231
232
// ---- Factory helpers --------------------------------------------------------
233
234
export function createRootState(): RootState {
235
return {
236
agents: [],
237
activeSessions: 0,
238
};
239
}
240
241
export function createSessionState(summary: SessionSummary): SessionState {
242
return {
243
summary,
244
lifecycle: SessionLifecycle.Creating,
245
turns: [],
246
activeTurn: undefined,
247
};
248
}
249
250
export function createActiveTurn(id: string, userMessage: UserMessage): ActiveTurn {
251
return {
252
id,
253
userMessage,
254
responseParts: [],
255
usage: undefined,
256
};
257
}
258
259
export const enum StateComponents {
260
Root,
261
Session,
262
Terminal,
263
}
264
265
export type ComponentToState = {
266
[StateComponents.Root]: RootState;
267
[StateComponents.Session]: SessionState;
268
[StateComponents.Terminal]: TerminalState;
269
};
270
271
// ---- SessionMeta accessors -------------------------------------------------
272
273
/**
274
* VS Code-side alias for the protocol's open `_meta` property bag on
275
* {@link SessionState}. Keys SHOULD be namespaced (e.g. `git`, `vscode.foo`)
276
* to avoid collisions; values MUST be JSON-serializable.
277
*/
278
export type SessionMeta = Record<string, unknown>;
279
280
/**
281
* Reserved key under {@link SessionMeta} for the well-known git-state
282
* payload. Value at this key, when present, MUST be shaped like
283
* {@link ISessionGitState}. This is a VS Code-specific convention layered
284
* on top of the protocol's generic `_meta` bag — the protocol itself does
285
* not know about git state.
286
*/
287
export const SESSION_META_GIT_KEY = 'git';
288
289
/**
290
* Git state of a session's working directory, carried under
291
* {@link SessionMeta} at {@link SESSION_META_GIT_KEY}. Used by clients to
292
* drive source-control affordances (e.g. PR/merge buttons in the Agents
293
* app).
294
*
295
* All fields are optional — agents that do not track a particular field
296
* should omit it rather than send a placeholder, so clients can distinguish
297
* "unknown" from "known to be zero".
298
*/
299
export interface ISessionGitState {
300
/** Whether the working directory has a `github.com` git remote. */
301
readonly hasGitHubRemote?: boolean;
302
/** Current branch name. */
303
readonly branchName?: string;
304
/** Base branch the work targets (e.g. `main`). */
305
readonly baseBranchName?: string;
306
/** Upstream tracking branch (e.g. `origin/feature`). */
307
readonly upstreamBranchName?: string;
308
/** Number of commits the upstream branch has ahead of the local branch. */
309
readonly incomingChanges?: number;
310
/** Number of commits the local branch has ahead of the upstream branch. */
311
readonly outgoingChanges?: number;
312
/** Number of files with uncommitted changes. */
313
readonly uncommittedChanges?: number;
314
}
315
316
/**
317
* Reads the well-known git-state payload from {@link SessionMeta}, if
318
* present. Returns `undefined` when the meta bag is absent or the value at
319
* the git key is not a plain object (e.g. an array or a primitive).
320
* Individual fields with wrong types are silently dropped so partial state
321
* still propagates.
322
*/
323
export function readSessionGitState(meta: SessionMeta | undefined): ISessionGitState | undefined {
324
const value = meta?.[SESSION_META_GIT_KEY];
325
if (!value || typeof value !== 'object' || Array.isArray(value)) {
326
return undefined;
327
}
328
const raw = value as Record<string, unknown>;
329
const result: {
330
hasGitHubRemote?: boolean;
331
branchName?: string;
332
baseBranchName?: string;
333
upstreamBranchName?: string;
334
incomingChanges?: number;
335
outgoingChanges?: number;
336
uncommittedChanges?: number;
337
} = {};
338
if (typeof raw['hasGitHubRemote'] === 'boolean') { result.hasGitHubRemote = raw['hasGitHubRemote']; }
339
if (typeof raw['branchName'] === 'string') { result.branchName = raw['branchName']; }
340
if (typeof raw['baseBranchName'] === 'string') { result.baseBranchName = raw['baseBranchName']; }
341
if (typeof raw['upstreamBranchName'] === 'string') { result.upstreamBranchName = raw['upstreamBranchName']; }
342
if (typeof raw['incomingChanges'] === 'number') { result.incomingChanges = raw['incomingChanges']; }
343
if (typeof raw['outgoingChanges'] === 'number') { result.outgoingChanges = raw['outgoingChanges']; }
344
if (typeof raw['uncommittedChanges'] === 'number') { result.uncommittedChanges = raw['uncommittedChanges']; }
345
return result;
346
}
347
348
/**
349
* Returns a new {@link SessionMeta} with the git-state payload set to
350
* `gitState`, or with the git slot removed if `gitState` is `undefined`.
351
* Returns `undefined` if the result would be empty.
352
*/
353
export function withSessionGitState(meta: SessionMeta | undefined, gitState: ISessionGitState | undefined): SessionMeta | undefined {
354
const next: { [key: string]: unknown } = { ...meta };
355
if (gitState !== undefined) {
356
next[SESSION_META_GIT_KEY] = gitState;
357
} else {
358
delete next[SESSION_META_GIT_KEY];
359
}
360
return Object.keys(next).length > 0 ? next : undefined;
361
}
362
363