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
106 changes: 99 additions & 7 deletions src/vs/platform/agentHost/node/claude/claudeAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ import { generateUuid } from '../../../../base/common/uuid.js';
import { localize } from '../../../../nls.js';
import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
import { ILogService } from '../../../log/common/log.js';
import { ISyncedCustomization } from '../../common/agentPluginManager.js';
import { IAgentPluginManager, ISyncedCustomization } from '../../common/agentPluginManager.js';
import { createSchema, platformSessionSchema, schemaProperty } from '../../common/agentHostSchema.js';
import { ClaudePermissionMode, ClaudeSessionConfigKey, narrowClaudePermissionMode } from '../../common/claudeSessionConfigKeys.js';
import { createClaudeThinkingLevelSchema, isClaudeEffortLevel } from '../../common/claudeModelConfig.js';
import { SessionConfigKey } from '../../common/sessionConfigKeys.js';
import { AgentProvider, AgentSession, AgentSignal, GITHUB_COPILOT_PROTECTED_RESOURCE, IAgent, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAgentSessionProjectInfo } from '../../common/agentService.js';
import { ActionType } from '../../common/state/sessionActions.js';
import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
import { AHP_AUTH_REQUIRED, ProtocolError } from '../../common/state/sessionProtocol.js';
import { PolicyState, ProtectedResourceMetadata, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js';
import { CustomizationRef, isSubagentSession, parseSubagentSessionUri, SessionInputResponseKind, type MessageAttachment, type PendingMessage, type SessionInputAnswer, type ToolCallResult, type Turn } from '../../common/state/sessionState.js';
import { PolicyState, ProtectedResourceMetadata, type AgentSelection, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js';
import { CustomizationRef, isSubagentSession, parseSubagentSessionUri, SessionInputResponseKind, type MessageAttachment, type PendingMessage, type SessionCustomization, type SessionInputAnswer, type ToolCallResult, type Turn } from '../../common/state/sessionState.js';
import { IAgentConfigurationService } from '../agentConfigurationService.js';
import { IAgentHostGitService } from '../agentHostGitService.js';
import { PendingRequestRegistry } from '../../common/pendingRequestRegistry.js';
Expand Down Expand Up @@ -141,6 +142,9 @@ export class ClaudeAgent extends Disposable implements IAgent {
private readonly _onDidSessionProgress = this._register(new Emitter<AgentSignal>());
readonly onDidSessionProgress = this._onDidSessionProgress.event;

private readonly _onDidCustomizationsChange = this._register(new Emitter<void>());
readonly onDidCustomizationsChange = this._onDidCustomizationsChange.event;

private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, []);
readonly models: IObservable<readonly IAgentModelInfo[]> = this._models;

Expand Down Expand Up @@ -224,6 +228,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
@IAgentHostGitService private readonly _gitService: IAgentHostGitService,
@IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IAgentPluginManager private readonly _pluginManager: IAgentPluginManager,
) {
super();
this._metadataStore = _instantiationService.createInstance(ClaudeSessionMetadataStore, this.id);
Expand Down Expand Up @@ -356,6 +361,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
config.workingDirectory,
project,
config.model,
config.agent,
config.config,
new PendingRequestRegistry<CallToolResult>(),
permissionMode,
Expand All @@ -364,6 +370,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
);
const entry = new ClaudeSessionEntry(session);
entry.addDisposable(session.onDidSessionProgress(signal => this._onDidSessionProgress.fire(signal)));
entry.addDisposable(session.onDidCustomizationsChange(() => this._onDidCustomizationsChange.fire()));
this._sessions.set(sessionId, entry);

return {
Expand Down Expand Up @@ -468,6 +475,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
workingDirectory,
project,
overlay.model,
overlay.agent,
undefined,
new PendingRequestRegistry<CallToolResult>(),
permissionMode,
Expand All @@ -476,6 +484,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
);
const entry = new ClaudeSessionEntry(session);
entry.addDisposable(session.onDidSessionProgress(signal => this._onDidSessionProgress.fire(signal)));
entry.addDisposable(session.onDidCustomizationsChange(() => this._onDidCustomizationsChange.fire()));
this._sessions.set(sessionId, entry);

const canUseTool: NonNullable<Options['canUseTool']> = (toolName, input, options) =>
Expand Down Expand Up @@ -882,6 +891,26 @@ export class ClaudeAgent extends Disposable implements IAgent {
});
}

/**
* Switch (or clear with `undefined`) the selected custom agent for an
* existing session. Mirrors {@link changeModel}: session owns its
* provisional/runtime branching and metadata write
* (see {@link ClaudeAgentSession.setAgent}). For external-only
* sessions (no in-memory record), the agent is persisted directly to
* the overlay so a later resume picks it up.
*/
async changeAgent(session: URI, agent: AgentSelection | undefined): Promise<void> {
const sessionId = AgentSession.id(session);
await this._sessionSequencer.queue(sessionId, async () => {
const sess = this._findAnySession(sessionId);
if (sess) {
await sess.setAgent(agent);
} else {
await this._metadataStore.write(session, { agent: agent ?? null });
}
});
}

setClientTools(session: URI, clientId: string, tools: ToolDefinition[]): void {
const sessionId = AgentSession.id(session);
this._logService.info(`[Claude:${sessionId}] setClientTools clientId=${clientId} tools=[${tools.map(t => t.name).join(', ') || '(none)'}]`);
Expand All @@ -908,12 +937,75 @@ export class ClaudeAgent extends Disposable implements IAgent {
entry?.session.completeClientToolCall(toolCallId, result);
}

setClientCustomizations(_session: URI, _clientId: string, _customizations: CustomizationRef[]): Promise<ISyncedCustomization[]> {
throw new Error('TODO: Phase 11');
async setClientCustomizations(session: URI, clientId: string, customizations: CustomizationRef[]): Promise<ISyncedCustomization[]> {
const sessionId = AgentSession.id(session);
const sess = this._findAnySession(sessionId);
if (!sess) {
this._logService.warn(`[Claude:${sessionId}] setClientCustomizations: session not found`);
return [];
}
// Run inside the session sequencer so that a fire-and-forget
// `setClientCustomizations` from `AgentSideEffects` cannot race
// ahead of a first `sendMessage`: if `sendMessage` is already
// queued, the sync runs first or queues behind it; either way
// the materialize call reads the most recently adopted plugin
// set, never an empty one mid-sync.
return this._sessionSequencer.queue(sessionId, async () => {
const synced = await this._pluginManager.syncCustomizations(
clientId,
customizations,
status => this._fireCustomizationUpdated(session, { customization: status }),
);
sess.adoptClientCustomizations(synced);
return synced;
});
}

/**
* Project a per-item sync result onto a `SessionCustomizationUpdated`
* action and emit it on {@link onDidSessionProgress}. Lets the workbench
* flip each row to `Loaded` / `Error` as the underlying
* {@link IAgentPluginManager.syncCustomizations} resolves it.
*/
private _fireCustomizationUpdated(session: URI, item: ISyncedCustomization): void {
this._onDidSessionProgress.fire({
kind: 'action',
session,
action: {
type: ActionType.SessionCustomizationUpdated,
customization: item.customization.customization,
enabled: item.customization.enabled,
...(item.customization.status !== undefined ? { status: item.customization.status } : {}),
...(item.customization.statusMessage !== undefined ? { statusMessage: item.customization.statusMessage } : {}),
...(item.customization.agents !== undefined ? { agents: item.customization.agents } : {}),
},
});
}

setCustomizationEnabled(uri: string, enabled: boolean): void {
for (const entry of this._sessions.values()) {
entry.session.setClientCustomizationEnabled(uri, enabled);
}
}

getCustomizations(): readonly CustomizationRef[] {
// Provider-level customization catalogue — feeds `AgentInfo.customizations`
// on `RootAgentsChanged`. Should advertise host-configured plugin refs
// (the equivalent of Copilot's `agentHost.customizations` setting).
// Claude has no such surface today; returning `[]` is correct rather
// than aggregating client-pushed refs (those live on
// `activeClient.customizations` per session).
//
// TODO: when host-level customizations become a real concept for the
// agent host, lift `PluginController` out of `copilot/copilotAgent.ts`
// into a shared service so both providers consume the same configured
// host customization list rather than each maintaining their own.
return [];
}

setCustomizationEnabled(_uri: string, _enabled: boolean): void {
throw new Error('TODO: Phase 11');
async getSessionCustomizations(session: URI): Promise<readonly SessionCustomization[]> {
const sess = this._findAnySession(AgentSession.id(session));
return sess ? await sess.getSessionCustomizations() : [];
}

// #endregion
Expand Down
Loading
Loading