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
16 changes: 14 additions & 2 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { IActionEnvelope, INotification, ISessionAction, ITerminalAction }
import type { IAgentSubscription } from './state/agentSubscription.js';
import type { ICreateTerminalParams } from './state/protocol/commands.js';
import type { IResourceCopyParams, IResourceCopyResult, IResourceDeleteParams, IResourceDeleteResult, IResourceListResult, IResourceMoveParams, IResourceMoveResult, IResourceReadResult, IResourceWriteParams, IResourceWriteResult, IStateSnapshot } from './state/sessionProtocol.js';
import { AttachmentType, ComponentToState, StateComponents, type ICustomizationRef, type IPendingMessage, type IRootState, type IToolCallResult, type PolicyState, type StringOrMarkdown } from './state/sessionState.js';
import { AttachmentType, ComponentToState, SessionStatus, StateComponents, type ICustomizationRef, type IPendingMessage, type IRootState, type ISessionInputAnswer, type ISessionInputRequest, type IToolCallResult, type PolicyState, type StringOrMarkdown, SessionInputResponseKind } from './state/sessionState.js';

// IPC contract between the renderer and the agent host utility process.
// Defines all serializable event types, the IAgent provider interface,
Expand Down Expand Up @@ -42,6 +42,7 @@ export interface IAgentSessionMetadata {
readonly startTime: number;
readonly modifiedTime: number;
readonly summary?: string;
readonly status?: SessionStatus;
readonly workingDirectory?: URI;
readonly isRead?: boolean;
readonly isDone?: boolean;
Expand Down Expand Up @@ -245,6 +246,12 @@ export interface IAgentSteeringConsumedEvent extends IAgentProgressEventBase {
readonly id: string;
}

/** The agent's ask_user tool is requesting user input. */
export interface IAgentUserInputRequestEvent extends IAgentProgressEventBase {
readonly type: 'user_input_request';
readonly request: ISessionInputRequest;
}

export type IAgentProgressEvent =
| IAgentDeltaEvent
| IAgentMessageEvent
Expand All @@ -256,7 +263,8 @@ export type IAgentProgressEvent =
| IAgentErrorEvent
| IAgentUsageEvent
| IAgentReasoningEvent
| IAgentSteeringConsumedEvent;
| IAgentSteeringConsumedEvent
| IAgentUserInputRequestEvent;

// ---- Session URI helpers ----------------------------------------------------

Expand Down Expand Up @@ -334,6 +342,9 @@ export interface IAgent {
/** Respond to a pending permission request from the SDK. */
respondToPermissionRequest(requestId: string, approved: boolean): void;

/** Respond to a pending user input request from the SDK's ask_user tool. */
respondToUserInputRequest(requestId: string, response: SessionInputResponseKind, answers?: Record<string, ISessionInputAnswer>): void;

/** Return the descriptor for this agent. */
getDescriptor(): IAgentDescriptor;

Expand Down Expand Up @@ -512,6 +523,7 @@ export interface IAgentConnection {
// ---- State subscriptions ------------------------------------------------
readonly rootState: IAgentSubscription<IRootState>;
getSubscription<T extends StateComponents>(kind: T, resource: URI): IReference<IAgentSubscription<ComponentToState[T]>>;
getSubscriptionUnmanaged<T extends StateComponents>(kind: T, resource: URI): IAgentSubscription<ComponentToState[T]> | undefined;

// ---- Action dispatch ----------------------------------------------------
dispatch(action: ISessionAction | ITerminalAction): void;
Expand Down
41 changes: 25 additions & 16 deletions src/vs/platform/agentHost/common/state/agentSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, IReference } from '../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../base/common/map.js';
import { URI } from '../../../../base/common/uri.js';
import { IActionEnvelope, ISessionAction, IStateAction, isSessionAction } from './sessionActions.js';
import { rootReducer, sessionReducer } from './sessionReducers.js';
Expand Down Expand Up @@ -350,7 +351,7 @@ export class TerminalStateSubscription extends BaseAgentSubscription<ITerminalSt
export class AgentSubscriptionManager extends Disposable {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly _subscriptions = new Map<string, { sub: BaseAgentSubscription<any>; refCount: number }>();
private readonly _subscriptions = new ResourceMap<{ sub: BaseAgentSubscription<any>; refCount: number }>();
private readonly _rootState: RootStateSubscription;
private readonly _clientId: string;
private readonly _seqAllocator: () => number;
Expand Down Expand Up @@ -387,43 +388,52 @@ export class AgentSubscriptionManager extends Disposable {
this._rootState.handleSnapshot(state, fromSeq);
}

/**
* Returns an existing subscription without affecting its refcount.
* Returns `undefined` if no subscription is active for the given resource.
*/
getSubscriptionUnmanaged<T>(resource: URI): IAgentSubscription<T> | undefined {
const entry = this._subscriptions.get(resource);
return entry?.sub as unknown as IAgentSubscription<T> | undefined;
}

/**
* Get or create a refcounted subscription to any resource. Disposing
* the returned reference decrements the refcount; when it reaches zero
* the subscription is torn down and the server is notified.
*/
getSubscription<T>(kind: StateComponents, resource: URI): IReference<IAgentSubscription<T>> {
const key = resource.toString();
const existing = this._subscriptions.get(key);
const existing = this._subscriptions.get(resource);
if (existing) {
existing.refCount++;
return {
object: existing.sub,
dispose: () => this._releaseSubscription(key),
dispose: () => this._releaseSubscription(resource),
};
}

// Create new subscription based on caller-specified kind
const key = resource.toString();
const sub = this._createSubscription(kind, key);
const entry = { sub, refCount: 1 };
this._subscriptions.set(key, entry);
this._subscriptions.set(resource, entry);

// Kick off server subscription asynchronously.
// Capture the entry reference so we can validate it hasn't been
// replaced by a new subscription for the same key (race guard).
this._subscribe(resource).then(snapshot => {
if (this._subscriptions.get(key) === entry) {
if (this._subscriptions.get(resource) === entry) {
sub.handleSnapshot(snapshot.state as never, snapshot.fromSeq);
}
}).catch(err => {
if (this._subscriptions.get(key) === entry) {
if (this._subscriptions.get(resource) === entry) {
sub.setError(err instanceof Error ? err : new Error(String(err)));
}
});

return {
object: sub,
dispose: () => this._releaseSubscription(key),
dispose: () => this._releaseSubscription(resource),
};
}

Expand All @@ -445,8 +455,7 @@ export class AgentSubscriptionManager extends Disposable {
*/
dispatchOptimistic(action: ISessionAction | ITerminalAction): number {
if (isSessionAction(action)) {
const key = action.session.toString();
const entry = this._subscriptions.get(key);
const entry = this._subscriptions.get(URI.parse(action.session));
if (entry && entry.sub instanceof SessionStateSubscription) {
return entry.sub.applyOptimistic(action);
}
Expand All @@ -466,15 +475,15 @@ export class AgentSubscriptionManager extends Disposable {
}
}

private _releaseSubscription(key: string): void {
const entry = this._subscriptions.get(key);
private _releaseSubscription(resource: URI): void {
const entry = this._subscriptions.get(resource);
if (!entry) {
return;
}
entry.refCount--;
if (entry.refCount <= 0) {
this._subscriptions.delete(key);
try { this._unsubscribe(URI.parse(key)); } catch { /* best-effort */ }
this._subscriptions.delete(resource);
try { this._unsubscribe(resource); } catch { /* best-effort */ }
if (entry.sub instanceof SessionStateSubscription) {
entry.sub.clearPending();
}
Expand All @@ -483,8 +492,8 @@ export class AgentSubscriptionManager extends Disposable {
}

override dispose(): void {
for (const [key, entry] of this._subscriptions) {
try { this._unsubscribe(URI.parse(key)); } catch { /* best-effort */ }
for (const [resource, entry] of this._subscriptions) {
try { this._unsubscribe(resource); } catch { /* best-effort */ }
entry.sub.dispose();
}
this._subscriptions.clear();
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
eea38f5
069f338
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// Generated from types/actions.ts — do not edit
// Run `npm run generate` to regenerate.

import { ActionType, type IStateAction, type IRootAgentsChangedAction, type IRootActiveSessionsChangedAction, type IRootTerminalsChangedAction, type ISessionReadyAction, type ISessionCreationFailedAction, type ISessionTurnStartedAction, type ISessionDeltaAction, type ISessionResponsePartAction, type ISessionToolCallStartAction, type ISessionToolCallDeltaAction, type ISessionToolCallReadyAction, type ISessionToolCallConfirmedAction, type ISessionToolCallCompleteAction, type ISessionToolCallResultConfirmedAction, type ISessionToolCallContentChangedAction, type ISessionTurnCompleteAction, type ISessionTurnCancelledAction, type ISessionErrorAction, type ISessionTitleChangedAction, type ISessionUsageAction, type ISessionReasoningAction, type ISessionModelChangedAction, type ISessionServerToolsChangedAction, type ISessionActiveClientChangedAction, type ISessionActiveClientToolsChangedAction, type ISessionPendingMessageSetAction, type ISessionPendingMessageRemovedAction, type ISessionQueuedMessagesReorderedAction, type ISessionCustomizationsChangedAction, type ISessionCustomizationToggledAction, type ISessionTruncatedAction, type ISessionIsReadChangedAction, type ISessionIsDoneChangedAction, type ISessionDiffsChangedAction, type ITerminalDataAction, type ITerminalInputAction, type ITerminalResizedAction, type ITerminalClaimedAction, type ITerminalTitleChangedAction, type ITerminalCwdChangedAction, type ITerminalExitedAction, type ITerminalClearedAction } from './actions.js';
import { ActionType, type IStateAction, type IRootAgentsChangedAction, type IRootActiveSessionsChangedAction, type IRootTerminalsChangedAction, type ISessionReadyAction, type ISessionCreationFailedAction, type ISessionTurnStartedAction, type ISessionDeltaAction, type ISessionResponsePartAction, type ISessionToolCallStartAction, type ISessionToolCallDeltaAction, type ISessionToolCallReadyAction, type ISessionToolCallConfirmedAction, type ISessionToolCallCompleteAction, type ISessionToolCallResultConfirmedAction, type ISessionToolCallContentChangedAction, type ISessionTurnCompleteAction, type ISessionTurnCancelledAction, type ISessionErrorAction, type ISessionTitleChangedAction, type ISessionUsageAction, type ISessionReasoningAction, type ISessionModelChangedAction, type ISessionServerToolsChangedAction, type ISessionActiveClientChangedAction, type ISessionActiveClientToolsChangedAction, type ISessionPendingMessageSetAction, type ISessionPendingMessageRemovedAction, type ISessionQueuedMessagesReorderedAction, type ISessionInputRequestedAction, type ISessionInputAnswerChangedAction, type ISessionInputCompletedAction, type ISessionCustomizationsChangedAction, type ISessionCustomizationToggledAction, type ISessionTruncatedAction, type ISessionIsReadChangedAction, type ISessionIsDoneChangedAction, type ISessionDiffsChangedAction, type ITerminalDataAction, type ITerminalInputAction, type ITerminalResizedAction, type ITerminalClaimedAction, type ITerminalTitleChangedAction, type ITerminalCwdChangedAction, type ITerminalExitedAction, type ITerminalClearedAction } from './actions.js';


// ─── Root vs Session vs Terminal Action Unions ───────────────────────────────
Expand Down Expand Up @@ -48,6 +48,9 @@ export type ISessionAction =
| ISessionPendingMessageSetAction
| ISessionPendingMessageRemovedAction
| ISessionQueuedMessagesReorderedAction
| ISessionInputRequestedAction
| ISessionInputAnswerChangedAction
| ISessionInputCompletedAction
| ISessionCustomizationsChangedAction
| ISessionCustomizationToggledAction
| ISessionTruncatedAction
Expand All @@ -70,6 +73,8 @@ export type IClientSessionAction =
| ISessionPendingMessageSetAction
| ISessionPendingMessageRemovedAction
| ISessionQueuedMessagesReorderedAction
| ISessionInputAnswerChangedAction
| ISessionInputCompletedAction
| ISessionCustomizationToggledAction
| ISessionTruncatedAction
| ISessionIsReadChangedAction
Expand All @@ -91,6 +96,7 @@ export type IServerSessionAction =
| ISessionUsageAction
| ISessionReasoningAction
| ISessionServerToolsChangedAction
| ISessionInputRequestedAction
| ISessionCustomizationsChangedAction
| ISessionDiffsChangedAction
;
Expand Down Expand Up @@ -158,6 +164,9 @@ export const IS_CLIENT_DISPATCHABLE: { readonly [K in IStateAction['type']]: boo
[ActionType.SessionPendingMessageSet]: true,
[ActionType.SessionPendingMessageRemoved]: true,
[ActionType.SessionQueuedMessagesReordered]: true,
[ActionType.SessionInputRequested]: false,
[ActionType.SessionInputAnswerChanged]: true,
[ActionType.SessionInputCompleted]: true,
[ActionType.SessionCustomizationsChanged]: false,
[ActionType.SessionCustomizationToggled]: true,
[ActionType.SessionTruncated]: true,
Expand Down
71 changes: 70 additions & 1 deletion src/vs/platform/agentHost/common/state/protocol/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// allow-any-unicode-comment-file
// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts

import { ToolCallConfirmationReason, ToolCallCancellationReason, PendingMessageKind, type URI, type StringOrMarkdown, type IAgentInfo, type IErrorInfo, type IUserMessage, type IResponsePart, type IToolCallResult, type IToolResultContent, type IToolDefinition, type ISessionActiveClient, type IUsageInfo, type ISessionCustomization, type ISessionFileDiff, type ITerminalInfo, type ITerminalClaim } from './state.js';
import { ToolCallConfirmationReason, ToolCallCancellationReason, PendingMessageKind, type URI, type StringOrMarkdown, type IAgentInfo, type IErrorInfo, type IUserMessage, type IResponsePart, type IToolCallResult, type IToolResultContent, type IToolDefinition, type ISessionActiveClient, type IUsageInfo, type ISessionCustomization, type ISessionFileDiff, type ISessionInputAnswer, type ISessionInputRequest, type ITerminalInfo, type ITerminalClaim, type SessionInputResponseKind } from './state.js';


// ─── Action Type Enum ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -44,6 +44,9 @@ export const enum ActionType {
SessionPendingMessageSet = 'session/pendingMessageSet',
SessionPendingMessageRemoved = 'session/pendingMessageRemoved',
SessionQueuedMessagesReordered = 'session/queuedMessagesReordered',
SessionInputRequested = 'session/inputRequested',
SessionInputAnswerChanged = 'session/inputAnswerChanged',
SessionInputCompleted = 'session/inputCompleted',
SessionCustomizationsChanged = 'session/customizationsChanged',
SessionCustomizationToggled = 'session/customizationToggled',
SessionTruncated = 'session/truncated',
Expand Down Expand Up @@ -754,6 +757,69 @@ export interface ISessionQueuedMessagesReorderedAction {
order: string[];
}

// ─── Session Input Actions ──────────────────────────────────────────────────

/**
* A session requested input from the user.
*
* Full-request upsert semantics: the `request` replaces any existing request
* with the same `id`, or is appended if it is new. Answer drafts are preserved
* unless `request.answers` is provided.
*
* @category Session Actions
* @version 1
*/
export interface ISessionInputRequestedAction {
type: ActionType.SessionInputRequested;
/** Session URI */
session: URI;
/** Input request to create or replace */
request: ISessionInputRequest;
}

/**
* A client updated, submitted, skipped, or removed a single in-progress answer.
*
* Dispatching with `answer: undefined` removes that question's answer draft.
*
* @category Session Actions
* @version 1
* @clientDispatchable
*/
export interface ISessionInputAnswerChangedAction {
type: ActionType.SessionInputAnswerChanged;
/** Session URI */
session: URI;
/** Input request identifier */
requestId: string;
/** Question identifier within the input request */
questionId: string;
/** Updated answer, or `undefined` to clear an answer draft */
answer?: ISessionInputAnswer;
}

/**
* A client accepted, declined, or cancelled a session input request.
*
* If accepted, the server uses `answers` (when provided) plus the request's
* synced answer state to resume the blocked operation.
*
* @category Session Actions
* @version 1
* @clientDispatchable
*/
export interface ISessionInputCompletedAction {
type: ActionType.SessionInputCompleted;
/** Session URI */
session: URI;
/** Input request identifier */
requestId: string;
/** Completion outcome */
response: SessionInputResponseKind;
/** Optional final answer replacement, keyed by question ID */
answers?: Record<string, ISessionInputAnswer>;
}

// ─── Terminal Actions ────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -931,6 +997,9 @@ export type IStateAction =
| ISessionPendingMessageSetAction
| ISessionPendingMessageRemovedAction
| ISessionQueuedMessagesReorderedAction
| ISessionInputRequestedAction
| ISessionInputAnswerChangedAction
| ISessionInputCompletedAction
| ISessionCustomizationsChangedAction
| ISessionCustomizationToggledAction
| ISessionTruncatedAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const enum NotificationType {
* "resource": "copilot:/<uuid>",
* "provider": "copilot",
* "title": "New Session",
* "status": "idle",
* "status": 1,
* "createdAt": 1710000000000,
* "modifiedAt": 1710000000000
* }
Expand Down
Loading
Loading