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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// DO NOT EDIT -- auto-generated by scripts/sync-agent-host-protocol.ts

import { ActionType } from '../common/actions.js';
import { SessionLifecycle, SessionStatus, CustomizationType, McpServerStatus, type SessionState, type SessionInputRequest, type McpServerCustomization } from './state.js';
import { SessionLifecycle, SessionStatus, SessionInputRequestKind, CustomizationType, McpServerStatus, type SessionState, type SessionInputRequest, type McpServerCustomization } from './state.js';
import type { SessionAction } from '../action-origin.generated.js';
import { softAssertNever } from '../common/reducer-helpers.js';

Expand All @@ -21,17 +21,29 @@ function withStatusFlag(status: SessionStatus, flag: SessionStatus, set: boolean
return set ? status | flag : status & ~flag;
}

/**
* Whether an entry blocks on the *user*.
*
* {@link SessionInputRequestKind.ToolClientExecution} is work delegated to a
* client, not a prompt: the call has already cleared its confirmation gate and
* is simply running somewhere else. Counting it would report a session as
* awaiting the user for the entire duration of every client tool call.
*/
function awaitsUser(request: SessionInputRequest): boolean {
return request.kind !== SessionInputRequestKind.ToolClientExecution;
}

/**
* Reflects the session-level {@link SessionState.inputNeeded | input queue}
* into the activity bits of `status`. A non-empty queue promotes the activity
* to {@link SessionStatus.InputNeeded}; emptying it clears the
* input-needed-specific bit. Since `InputNeeded` implies
* into the activity bits of `status`. A queue holding any user-blocking entry
* promotes the activity to {@link SessionStatus.InputNeeded}; draining those
* entries clears the input-needed-specific bit. Since `InputNeeded` implies
* {@link SessionStatus.InProgress}, an unblocked turn falls back to
* `InProgress` while an already-idle session stays idle. Orthogonal flags
* (`IsRead` / `IsArchived`) are preserved.
*/
function withInputNeededStatus(status: SessionStatus, inputNeeded: readonly SessionInputRequest[]): SessionStatus {
if (inputNeeded.length > 0) {
if (inputNeeded.some(awaitsUser)) {
return (status & ~STATUS_ACTIVITY_MASK) | SessionStatus.InputNeeded;
}
return status & ~(SessionStatus.InputNeeded & ~SessionStatus.InProgress);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ export interface SessionState extends SessionMetadata {
* Each entry is self-sufficient: it carries the owning chat's URI plus every
* identifier the client needs to respond. A client answers by dispatching the
* ordinary `chat/*` action to that chat's channel — see
* {@link SessionInputRequest} for the per-variant response path. A present,
* non-empty list implies {@link SessionStatus.InputNeeded} on
* {@link SessionSummary.status}.
* {@link SessionInputRequest} for the per-variant response path. A list
* holding any entry other than
* {@link SessionInputRequestKind.ToolClientExecution} implies
* {@link SessionStatus.InputNeeded} on {@link SessionSummary.status};
* client-execution entries are work delegated to a client rather than a
* prompt, so they leave the session's activity unchanged.
*
* Host-managed: the host upserts entries with `session/inputNeededSet` as
* chats raise requests and removes them with `session/inputNeededRemoved`
Expand Down
13 changes: 6 additions & 7 deletions src/vs/platform/agentHost/node/agentSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,12 +485,11 @@ export class AgentSideEffects extends Disposable {
const authenticationId = this._toolAuthenticationNeededId(chatUri, turnId, toolCallId);
const toolCall = this._findToolCall(chatUri, turnId, toolCallId);

// A call auto-approved by the session's bypass setting is run
// automatically by the owning client and never blocks on the user, so
// keep it out of the session `inputNeeded` queue (which would flash
// "input needed" in the sessions list). `autoApproveBySetting` covers
// only the parameter gate; a `PendingResultConfirmation` is a genuine
// prompt and is still surfaced.
// A parameter gate auto-approved by the session's bypass setting never
// blocks on the user, so keep it out of the session `inputNeeded` queue
// (which would flash "input needed" in the sessions list).
// `autoApproveBySetting` covers only the parameter gate; a
// `PendingResultConfirmation` is a genuine prompt and is still surfaced.
const autoApproved = !!toolCall && readToolCallMeta(toolCall).autoApproveBySetting === true;

const suppressAutoApprovedConfirmation = autoApproved && toolCall?.status === ToolCallStatus.PendingConfirmation;
Expand All @@ -508,7 +507,7 @@ export class AgentSideEffects extends Disposable {
}

const contributor = toolCall?.contributor;
if (!autoApproved && toolCall?.status === ToolCallStatus.Running && contributor?.kind === ToolCallContributorKind.Client) {
if (toolCall?.status === ToolCallStatus.Running && contributor?.kind === ToolCallContributorKind.Client) {
this._setSessionInputNeeded(chatUri, {
id: clientExecutionId,
kind: SessionInputRequestKind.ToolClientExecution,
Expand Down
16 changes: 14 additions & 2 deletions src/vs/platform/agentHost/test/node/agentSideEffects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4975,6 +4975,10 @@ suite('AgentSideEffects', () => {
return stateManager.getSessionState(sessionUri.toString())?.inputNeeded ?? [];
}

function sessionStatus() {
return stateManager.getSessionState(sessionUri.toString())?.status;
}

test('chat input request mirrors its unresolved response part and is removed on completion', () => {
setupSession();
startTurn('turn-1');
Expand Down Expand Up @@ -5178,7 +5182,7 @@ suite('AgentSideEffects', () => {
assert.deepStrictEqual(sessionInputNeeded(), []);
});

test('auto-approved tool call is kept out of the session inputNeeded queue', () => {
test('auto-approved tool call still surfaces its client execution without flagging input needed', () => {
setupSession();
startTurn('turn-1');

Expand All @@ -5200,7 +5204,15 @@ suite('AgentSideEffects', () => {
type: ActionType.ChatToolCallConfirmed, turnId: 'turn-1',
toolCallId: 'tc-auto', approved: true, confirmed: ToolCallConfirmationReason.Setting,
});
assert.deepStrictEqual(sessionInputNeeded(), [], 'no client-execution entry while Running');

// The client still has to run the call, so it must be discoverable
// from the session channel — but it is not a user prompt, so the
// session must not present as "input needed".
assert.deepStrictEqual(
sessionInputNeeded().map(r => ({ kind: r.kind, clientId: r.kind === SessionInputRequestKind.ToolClientExecution ? r.clientId : undefined })),
[{ kind: SessionInputRequestKind.ToolClientExecution, clientId: 'client-1' }],
);
assert.strictEqual(sessionStatus(), SessionStatus.InProgress, 'auto-approved client execution must not present as input needed');
});

test('auto-approved tool still surfaces a genuine result confirmation', () => {
Expand Down
Loading
Loading