Skip to content
10 changes: 4 additions & 6 deletions src/vs/platform/agentHost/node/agentHostAutomationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,12 +754,10 @@ export class AgentHostAutomationService extends Disposable implements IAgentHost
await this._execution.cancelSession(session);
return;
}
// Turn selections override creation defaults in both the provider and restored clients.
const message: Message = {
...definition.message,
...(definition.message.model === undefined && definition.session.model !== undefined ? { model: definition.session.model } : {}),
...(definition.message.agent === undefined && definition.session.agent !== undefined ? { agent: definition.session.agent } : {}),
};
// Clients restore the last turn's model configuration, not the SDK's creation defaults.
const message: Message = definition.message.model === undefined && definition.session.model !== undefined
? { ...definition.message, model: definition.session.model }
: definition.message;
await this._execution.startSession(session, message);
} catch (error) {
try {
Expand Down
51 changes: 7 additions & 44 deletions src/vs/platform/agentHost/node/codex/codexAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,10 @@ import { IAgentHostProxyResolver } from '../agentHostProxyResolver.js';
import { MODEL_REFRESH_BASE_DELAY_MS, MODEL_REFRESH_MAX_ATTEMPTS, MODEL_REFRESH_MAX_DELAY_MS, modelRefreshBackoff } from '../shared/modelRefreshRetry.js';
import { AGENT_HOST_WORKSPACELESS_INSTRUCTIONS } from '../shared/workspacelessInstructions.js';
import { IAgentHostCheckpointService } from '../../common/agentHostCheckpointService.js';
import { IAgentHostGitService, tryResolvePrimaryWorktreeRoot } from '../../common/agentHostGitService.js';
import { ISessionDataService } from '../../common/sessionDataService.js';
import { ICopilotApiService } from '../shared/copilotApiService.js';
import { extractForwardedErrorInfo } from '../shared/proxyChatError.js';
import { IAgentHostWorktreeIsolation } from '../shared/worktreeIsolation.js';
import { IAgentHostWorktreeIsolation, type IAgentHostWorktreePendingState } from '../shared/worktreeIsolation.js';
import { getServerToolDisplay } from '../shared/serverToolGroups.js';
import { IAgentSdkDownloader, IAgentSdkPackage } from '../agentSdkDownloader.js';
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
Expand Down Expand Up @@ -1253,6 +1252,7 @@ export class CodexAgent extends Disposable implements IAgent {
private readonly _metadataStore: CodexSessionMetadataStore;
private _lastSignInRequest: string | undefined;
private _lastSignOutRequest: string | undefined;
private readonly _worktree: IAgentHostWorktreePendingState;

/**
* The agent host's server-tool host (feedback "comments" today, more in the
Expand Down Expand Up @@ -1280,12 +1280,12 @@ export class CodexAgent extends Disposable implements IAgent {
@IAgentHostOTelService private readonly _otelService: IAgentHostOTelService,
@IAgentHostCustomizationEnablementService private readonly _customizationEnablementService: IAgentHostCustomizationEnablementService,
@IAgentHostSessionTitleSignal sessionTitleSignal: IAgentHostSessionTitleSignal,
@IAgentHostWorktreeIsolation private readonly _worktree: IAgentHostWorktreeIsolation,
@IAgentHostWorktreeIsolation worktree: IAgentHostWorktreeIsolation,
@ISessionDataService private readonly _sessionDataService: ISessionDataService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IAgentHostGitService private readonly _gitService: IAgentHostGitService,
) {
super();
this._worktree = worktree;
this._metadataStore = this._instantiationService.createInstance(CodexSessionMetadataStore);
this._githubMcpServerEnabled = this._isGitHubMcpServerEnabled();
this._publishAccountInfo({ status: 'unknown' });
Expand Down Expand Up @@ -1927,50 +1927,13 @@ export class CodexAgent extends Disposable implements IAgent {
return resolved.filter(candidate => candidate !== undefined);
}

/** Resolve native workspace agents in the host-owned worktree without rewriting their persisted selection identity. */
private async _resolveSelectedAgent(session: ICodexSession): Promise<AgentSelection | undefined> {
const agent = session.agent;
if (!agent || !session.workingDirectory) {
return agent;
}
const agentUri = URI.parse(agent.uri);
const agentsDirectory = extUriBiasedIgnorePathCase.dirname(agentUri);
const sourceRoot = extUriBiasedIgnorePathCase.dirname(extUriBiasedIgnorePathCase.dirname(agentsDirectory));
if (!extUriBiasedIgnorePathCase.isEqual(agentsDirectory, URI.joinPath(sourceRoot, '.github', 'agents'))
|| extUriBiasedIgnorePathCase.isEqual(sourceRoot, session.workingDirectory)) {
return agent;
}
const worktree = await this._worktree.readWorktreeMetadata(session.configurationResource);
if (!worktree?.repositoryRoot || !worktree.worktreePath
|| !extUriBiasedIgnorePathCase.isEqual(session.workingDirectory, worktree.worktreePath)) {
return agent;
}
if (!extUriBiasedIgnorePathCase.isEqual(sourceRoot, worktree.repositoryRoot)) {
try {
const checkoutRoot = await this._gitService.getRepositoryRoot(sourceRoot);
if (!checkoutRoot || !extUriBiasedIgnorePathCase.isEqual(checkoutRoot, sourceRoot)
|| !extUriBiasedIgnorePathCase.isEqual(await tryResolvePrimaryWorktreeRoot(this._gitService, checkoutRoot), worktree.repositoryRoot)) {
return agent;
}
} catch (error) {
this._logService.warn('[Codex] Failed to resolve the selected workspace agent repository', error);
return agent;
}
}
return {
...agent,
uri: URI.joinPath(worktree.worktreePath, '.github', 'agents', extUriBiasedIgnorePathCase.basename(agentUri)).toString(),
};
}

private async _buildCustomizationLaunch(session: ICodexSession): Promise<ICodexCustomizationLaunch> {
const plugins = this._enabledClientPlugins(session);
const [workspaceAgents, workspaceSkills, selectedAgent] = await Promise.all([
const [workspaceAgents, workspaceSkills] = await Promise.all([
discoverCodexWorkspaceAgents(this._customizationWorkingDirectories(session), this._fileService),
discoverCodexWorkspaceSkills(this._customizationWorkingDirectories(session), this._fileService),
this._resolveSelectedAgent(session),
]);
const customization = await codexCustomizationConfig(workspaceAgents.agents, plugins, selectedAgent, this._fileService);
const customization = await codexCustomizationConfig(workspaceAgents.agents, plugins, session.agent, this._fileService);
const developerInstructions = [
customization.developerInstructions,
session.managedWorkingDirectory ? AGENT_HOST_WORKSPACELESS_INSTRUCTIONS : '',
Expand Down Expand Up @@ -2002,7 +1965,7 @@ export class CodexAgent extends Disposable implements IAgent {
})),
];
const signature = JSON.stringify({
agent: selectedAgent?.uri,
agent: session.agent?.uri,
agentRoles: customization.agentRoles,
developerInstructions,
selectedCapabilityRoots: selectedCapabilityRoots.map(root => root.location.path),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -725,16 +725,13 @@ suite('AgentHostAutomationService', () => {
})));
});

for (const messageOverride of ['none', 'model', 'agent', 'both']) {
test(`records the Automation session selections on its first turn with ${messageOverride} message overrides`, async () => {
for (const hasMessageModel of [false, true]) {
test(hasMessageModel ? 'preserves an explicit Automation message model' : 'records the Automation model configuration on its first turn', async () => {
const session = URI.parse('mock:/model-configuration-run');
const model = { id: 'mock-model', config: { thinkingLevel: 'low', contextSize: 272_000 } };
const agent = { uri: 'file:///workspace/.github/agents/reviewer.agent.md' };
const messageModel = messageOverride === 'model' || messageOverride === 'both' ? { id: 'other-model', config: { thinkingLevel: 'high' } } : undefined;
const messageAgent = messageOverride === 'agent' || messageOverride === 'both' ? { uri: 'file:///other/agents/reviewer.agent.md' } : undefined;
const messageModel = hasMessageModel ? { id: 'other-model', config: { thinkingLevel: 'high' } } : undefined;
const completed = new DeferredPromise<void>();
let createdModel: AutomationDefinition['session']['model'];
let createdAgent: AutomationDefinition['session']['agent'];
disposables.add(stateManager.onDidEmitEnvelope(envelope => {
if (envelope.action.type === ActionType.AutomationRunLifecycleChanged && envelope.action.lifecycle.status === AutomationRunStatus.Completed) {
void completed.complete();
Expand All @@ -743,7 +740,6 @@ suite('AgentHostAutomationService', () => {
const service = createService({
createSession: async template => {
createdModel = template.model;
createdAgent = template.agent;
stateManager.createSession({
resource: session.toString(),
provider: 'mock',
Expand Down Expand Up @@ -771,13 +767,9 @@ suite('AgentHostAutomationService', () => {
});
const automation = definition();
automation.session.model = model;
automation.session.agent = agent;
if (messageModel) {
automation.message.model = messageModel;
}
if (messageAgent) {
automation.message.agent = messageAgent;
}
await service.completeMigration();
await service.handleCreate({ ...createAction(), definition: automation });
await service.runAutomation({
Expand All @@ -789,24 +781,12 @@ suite('AgentHostAutomationService', () => {

assert.deepStrictEqual({
createdModel,
createdAgent,
recordedModel: stateManager.getChatState(buildDefaultChatUri(session))?.turns[0]?.message.model,
recordedAgent: stateManager.getChatState(buildDefaultChatUri(session))?.turns[0]?.message.agent,
savedModel: stateManager.getAutomationCatalogState()?.entries[0].definition.session.model,
savedAgent: stateManager.getAutomationCatalogState()?.entries[0].definition.session.agent,
savedMessage: stateManager.getAutomationCatalogState()?.entries[0].definition.message,
}, {
createdModel: model,
createdAgent: agent,
recordedModel: messageModel ?? model,
recordedAgent: messageAgent ?? agent,
savedModel: model,
savedAgent: agent,
savedMessage: {
...definition().message,
...(messageModel ? { model: messageModel } : {}),
...(messageAgent ? { agent: messageAgent } : {}),
},
});
});
}
Expand Down
Loading