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