Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib
longContextCacheWriteCost: endpoint instanceof AutoChatEndpoint ? undefined : endpoint.tokenPricing?.longContext?.cacheWriteTokenPrice,
multiplierNumeric: endpoint instanceof AutoChatEndpoint ? undefined : endpoint.multiplier,
priceCategory: endpoint instanceof AutoChatEndpoint ? undefined : endpoint.priceCategory,
category: endpoint instanceof AutoChatEndpoint ? undefined : endpoint.modelPickerCategory,
detail: modelDetail,
statusIcon: endpoint.degradationReason ? new vscode.ThemeIcon('warning') : undefined,
version: endpoint.version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export interface IModelAPIResponse {
info_messages?: { code: string; message: string }[];
billing?: IModelBilling;
model_picker_price_category?: string;
model_picker_category?: string;
capabilities: IChatModelCapabilities | ICompletionModelCapabilities | IEmbeddingModelCapabilities;
supported_endpoints?: ModelSupportedEndpoint[];
custom_model?: CustomModel;
Expand Down
2 changes: 2 additions & 0 deletions extensions/copilot/src/platform/endpoint/node/chatEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export class ChatEndpoint implements IChatEndpoint {
public readonly restrictedToSkus?: string[] | undefined;
public readonly tokenPricing?: IChatEndpointTokenPricing | undefined;
public readonly priceCategory?: string | undefined;
public readonly modelPickerCategory?: string | undefined;
public readonly customModel?: CustomModel | undefined;
public readonly maxPromptImages?: number | undefined;

Expand Down Expand Up @@ -175,6 +176,7 @@ export class ChatEndpoint implements IChatEndpoint {
longContext: normalized.longContext ? { inputPrice: normalized.longContext.inputPrice, outputPrice: normalized.longContext.outputPrice, cacheReadTokenPrice: normalized.longContext.cachePrice, cacheWriteTokenPrice: normalized.longContext.cacheWritePrice, contextMax: normalized.longContext.contextMax } : undefined,
} : undefined;
this.priceCategory = modelMetadata.model_picker_price_category;
this.modelPickerCategory = modelMetadata.model_picker_category;
this.isFallback = modelMetadata.is_chat_fallback;
this.supportsToolCalls = !!modelMetadata.capabilities.supports.tool_calls;
this.supportsVision = !!modelMetadata.capabilities.supports.vision;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ export interface IChatEndpoint extends IEndpoint {
*/
readonly tokenPricing?: IChatEndpointTokenPricing;
readonly priceCategory?: string;
readonly modelPickerCategory?: string;
readonly isFallback: boolean;
readonly customModel?: CustomModel;
readonly isExtensionContributed?: boolean;
Expand Down
42 changes: 36 additions & 6 deletions src/vs/platform/agentHost/common/agentHostSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,9 @@ function safeStringify(value: unknown): string {

// ---- Platform-owned schema -------------------------------------------------

export type AutoApproveLevel = 'default' | 'autoApprove' | 'autopilot';
export type AutoApproveLevel = 'default' | 'autoApprove';

export type SessionMode = 'interactive' | 'plan';
export type SessionMode = 'interactive' | 'plan' | 'autopilot';

export interface IPermissionsValue {
readonly allow: readonly string[];
Expand Down Expand Up @@ -307,16 +307,14 @@ export const platformSessionSchema = createSchema({
type: 'string',
title: localize('agentHost.sessionConfig.autoApprove', "Approvals"),
description: localize('agentHost.sessionConfig.autoApproveDescription', "Tool approval behavior for this session"),
enum: ['default', 'autoApprove', 'autopilot'],
enum: ['default', 'autoApprove'],
enumLabels: [
localize('agentHost.sessionConfig.autoApprove.default', "Default Approvals"),
localize('agentHost.sessionConfig.autoApprove.bypass', "Bypass Approvals"),
localize('agentHost.sessionConfig.autoApprove.autopilot', "Autopilot (Preview)"),
],
enumDescriptions: [
localize('agentHost.sessionConfig.autoApprove.defaultDescription', "Copilot uses your configured settings"),
localize('agentHost.sessionConfig.autoApprove.bypassDescription', "All tool calls are auto-approved"),
localize('agentHost.sessionConfig.autoApprove.autopilotDescription', "Autonomously iterates from start to finish"),
],
default: 'default',
sessionMutable: true,
Expand All @@ -326,20 +324,52 @@ export const platformSessionSchema = createSchema({
type: 'string',
title: localize('agentHost.sessionConfig.mode', "Agent Mode"),
description: localize('agentHost.sessionConfig.modeDescription', "How the agent should approach this turn"),
enum: ['interactive', 'plan'],
enum: ['interactive', 'plan', 'autopilot'],
enumLabels: [
localize('agentHost.sessionConfig.mode.interactive', "Interactive"),
localize('agentHost.sessionConfig.mode.plan', "Plan"),
localize('agentHost.sessionConfig.mode.autopilot', "Autopilot"),
],
enumDescriptions: [
localize('agentHost.sessionConfig.mode.interactiveDescription', "Step-by-step collaboration"),
localize('agentHost.sessionConfig.mode.planDescription', "Plan first, execute when ready"),
localize('agentHost.sessionConfig.mode.autopilotDescription', "Autonomously iterates from start to finish"),
],
default: 'interactive',
sessionMutable: true,
}),
});

/**
* Rewrites a legacy `autoApprove='autopilot'` config value — used before
* Autopilot moved from the `autoApprove` axis onto the orthogonal `mode`
* axis — into the current two-axis shape:
*
* - `autoApprove='autopilot'` + `mode='plan'` → `mode='plan'`, `autoApprove='default'`
* (legacy `plan` took precedence over autopilot when resolving the SDK mode).
* - `autoApprove='autopilot'` + any other mode → `mode='autopilot'`, `autoApprove='default'`.
*
* Returns a shallow copy with the migration applied, or the original
* reference unchanged when no legacy value is present. Safe to call on
* `undefined`.
*
* Without this, a session persisted (or a "remembered" picker value seeded)
* with `autoApprove='autopilot'` would fail the new schema's enum validation
* and silently fall back to `default`, downgrading the session from
* autonomous Autopilot to manual per-tool confirmation.
*/
export function migrateLegacyAutopilotConfig<T extends Record<string, unknown> | undefined>(config: T): T {
if (!config || config[SessionConfigKey.AutoApprove] !== 'autopilot') {
return config;
}
const migrated: Record<string, unknown> = { ...config };
if (migrated[SessionConfigKey.Mode] !== 'plan') {
migrated[SessionConfigKey.Mode] = 'autopilot' satisfies SessionMode;
}
migrated[SessionConfigKey.AutoApprove] = 'default' satisfies AutoApproveLevel;
return migrated as T;
}

/**
* Root (agent host) config properties owned by the platform itself.
*
Expand Down
24 changes: 18 additions & 6 deletions src/vs/platform/agentHost/common/sessionConfigKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,27 @@ export const enum SessionConfigKey {
Isolation = 'isolation',
/** `'branch'` — base branch to work from. */
Branch = 'branch',
/** `'mode'` — agent execution mode (interactive / plan). */
/** `'mode'` — agent execution mode (interactive / plan / autopilot). */
Mode = 'mode',
}

/**
* The set of enum values the unified permission picker understands for the
* {@link SessionConfigKey.AutoApprove} property.
* The set of enum values the unified permission picker *tolerates* for the
* {@link SessionConfigKey.AutoApprove} property when deciding whether a
* session's schema is "well-known" (and therefore handled by the dedicated
* permission picker rather than the generic per-property fallback).
*
* `default` is the required baseline level; `autoApprove` and `autopilot`
* are optional (an agent may choose to advertise a subset).
* `default` is the required baseline level; `autoApprove` is the offered
* elevated level. `assisted` and `autopilot` are retained here purely for
* backward/forward compatibility so a session whose schema was resolved by an
* older or newer agent host (advertising those values) still renders the
* dedicated picker rather than disappearing. The picker itself only ever
* *offers* `default` / `autoApprove` (see the delegate's `availableLevels`).
*/
export const KNOWN_AUTO_APPROVE_VALUES: ReadonlySet<string> = new Set(['default', 'autoApprove', 'autopilot']);
export const KNOWN_AUTO_APPROVE_VALUES: ReadonlySet<string> = new Set(['default', 'assisted', 'autoApprove', 'autopilot']);

/**
* The set of enum values understood for the {@link SessionConfigKey.Mode}
* property: the agent execution mode axis.
*/
export const KNOWN_MODE_VALUES: ReadonlySet<string> = new Set(['interactive', 'plan', 'autopilot']);
16 changes: 15 additions & 1 deletion src/vs/platform/agentHost/node/agentHostStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { TelemetryLevel } from '../../telemetry/common/telemetry.js';
import { ActionType, ActionEnvelope, ActionOrigin, INotification, IRootConfigChangedAction, SessionAction, ChatAction, RootAction, StateAction, TerminalAction, ChangesetAction, AnnotationsAction, ClientAnnotationsAction, isRootAction, isSessionAction, isChatAction, isChangesetAction, isAnnotationsAction } from '../common/state/sessionActions.js';
import type { IStateSnapshot } from '../common/state/sessionProtocol.js';
import { rootReducer, sessionReducer, chatReducer, changesetReducer, annotationsReducer } from '../common/state/sessionReducers.js';
import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, isAhpChatChannel, isDefaultChatUri, mergeSessionWithDefaultChat, isAhpRootChannel, SessionLifecycle, withHostBuildInfo, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type ISessionWithDefaultChat, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, IHostBuildInfo, SessionStatus } from '../common/state/sessionState.js';
import { createRootState, createSessionState, createChatState, createDefaultChatSummary, chatSummaryFromState, buildDefaultChatUri, parseDefaultChatUri, isAhpChatChannel, isDefaultChatUri, mergeSessionWithDefaultChat, isAhpRootChannel, SessionLifecycle, withHostBuildInfo, type Changeset, type ChangesetState, type AnnotationsState, type ChatState, type ChatSummary, type Customization, type ISessionWithDefaultChat, type RootState, type SessionConfigState, type SessionMeta, type SessionState, type SessionSummary, type Turn, type URI, ROOT_STATE_URI, ChangesetStatus, IHostBuildInfo, SessionStatus } from '../common/state/sessionState.js';
import { AgentHostTelemetryLevelConfigKey, IPermissionsValue, platformRootSchema, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';
import { SessionConfigKey } from '../common/sessionConfigKeys.js';
import { parseChangesetUri } from '../common/changesetUri.js';
Expand Down Expand Up @@ -666,6 +666,20 @@ export class AgentHostStateManager extends Disposable {
state.config = config;
}

/**
* Seeds or replaces the session's effective customizations directly on the
* authoritative in-memory state. Used by create/restore flows to ensure the
* first snapshot already contains customizations.
*/
setSessionCustomizations(session: URI, customizations: readonly Customization[] | undefined): void {
const state = this._sessionStates.get(session);
if (!state) {
this._logService.warn(`[AgentHostStateManager] setSessionCustomizations: unknown session ${session}`);
return;
}
state.customizations = customizations ? [...customizations] : undefined;
}

// ---- Changeset registry -------------------------------------------------

/**
Expand Down
22 changes: 18 additions & 4 deletions src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1553,13 +1553,27 @@ export class AgentService extends Disposable implements IAgentService {
// sessions that were not created in the current process lifetime.
// Overlay any values the user previously selected (persisted via
// `SessionConfigChanged`) on top of the provider's resolved defaults.
const restoredConfig = await this._resolveCreatedSessionConfig(agent, {
workingDirectory: meta.workingDirectory,
config: persistedConfigValues,
});
const [restoredConfig, restoredCustomizations] = await Promise.all([
this._resolveCreatedSessionConfig(agent, {
workingDirectory: meta.workingDirectory,
config: persistedConfigValues,
}),
agent.getSessionCustomizations
? agent.getSessionCustomizations(session).catch(err => {
this._logService.error('[AgentService] restoreSession: failed to resolve session customizations', err);
return undefined;
})
: Promise.resolve(undefined),
]);
if (restoredConfig) {
this._stateManager.setSessionConfig(sessionStr, restoredConfig);
}
// Seed restored session customizations into state so the very first
// snapshot after selecting an existing session contains effective
// instructions/agents without waiting for a follow-up republish.
if (restoredCustomizations && restoredCustomizations.length > 0) {
this._stateManager.setSessionCustomizations(sessionStr, restoredCustomizations);
}

this._logService.info(`[AgentService] Restored session ${sessionStr} with ${turns.length} turns`);

Expand Down
41 changes: 22 additions & 19 deletions src/vs/platform/agentHost/node/copilot/copilotAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { ILogService, LogLevel } from '../../../log/common/log.js';
import { IAgentHostCheckpointService } from '../../common/agentHostCheckpointService.js';
import { createAgentModelPricingMeta } from '../../common/agentModelPricing.js';
import { AgentHostConfigKey, agentHostCustomizationConfigSchema, toContainerCustomization } from '../../common/agentHostCustomizationConfig.js';
import { AgentHostSessionSyncEnabledConfigKey, AutoApproveLevel, ISchemaProperty, SessionMode, createSchema, platformRootSchema, platformSessionSchema, schemaProperty } from '../../common/agentHostSchema.js';
import { AgentHostSessionSyncEnabledConfigKey, AutoApproveLevel, ISchemaProperty, SessionMode, createSchema, migrateLegacyAutopilotConfig, platformRootSchema, platformSessionSchema, schemaProperty } from '../../common/agentHostSchema.js';
import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js';
import { AgentSession, AgentSignal, GITHUB_COPILOT_PROTECTED_RESOURCE, IAgent, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSessionProjectInfo, IMcpNotification } from '../../common/agentService.js';
import { getEffectiveAgents } from '../../common/customAgents.js';
Expand Down Expand Up @@ -1240,7 +1240,7 @@ export class CopilotAgent extends Disposable implements IAgent {
...(branchProperty ? { [SessionConfigKey.Branch]: branchProperty } : {}),
});

const values = sessionSchema.validateOrDefault(params.config, {
const values = sessionSchema.validateOrDefault(migrateLegacyAutopilotConfig(params.config), {
[SessionConfigKey.Isolation]: isolationValue,
[SessionConfigKey.AutoApprove]: 'default' satisfies AutoApproveLevel,
[SessionConfigKey.Mode]: 'interactive' satisfies SessionMode,
Expand Down Expand Up @@ -1380,32 +1380,35 @@ export class CopilotAgent extends Disposable implements IAgent {
}

/**
* Translates the AHP-side `(mode, autoApprove)` pair to the Copilot
* SDK's three-mode space (`interactive` / `plan` / `autopilot`):
* Translates the AHP-side `mode` to the Copilot SDK's three-mode space
* (`interactive` / `plan` / `autopilot`). With Autopilot living on the
* `mode` axis the mapping is now direct:
*
* - `mode='plan'` → SDK `plan` (auto-approval is irrelevant; the
* agent host's existing session-state auto-approval logic handles
* `plan.md` writes).
* - `mode='interactive'` + `autoApprove='autopilot'` → SDK `autopilot`
* (the SDK auto-approves all tool calls).
* - `mode='interactive'` + any other autoApprove → SDK `interactive`
* (the agent host's own auto-approval logic continues to gate tool
* calls based on `autoApprove`).
* - `mode='plan'` → SDK `plan`.
* - `mode='autopilot'` → SDK `autopilot` (autonomous, continue-until-done).
* - `mode='interactive'` → SDK `interactive`.
*
* Tool auto-approval is governed independently by the orthogonal
* `autoApprove` axis (Default / Bypass), enforced by the agent
* host's own permission handler — which the SDK still invokes even under
* autopilot mode.
*
* Returns `undefined` when no mode is configured for the session, so
* the SDK's current mode is left untouched.
*/
private _resolveSdkMode(session: URI): CopilotSdkMode | undefined {
const sessionKey = session.toString();
const mode = this._configurationService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Mode);
if (mode === 'plan') {
return 'plan';
}
if (mode === 'interactive') {
const autoApprove = this._configurationService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.AutoApprove);
return autoApprove === 'autopilot' ? 'autopilot' : 'interactive';
switch (mode) {
case 'plan':
return 'plan';
case 'autopilot':
return 'autopilot';
case 'interactive':
return 'interactive';
default:
return undefined;
}
return undefined;
}

setPendingMessages(session: URI, steeringMessage: PendingMessage | undefined, _queuedMessages: readonly PendingMessage[]): void {
Expand Down
Loading
Loading