Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -36,6 +36,7 @@ export class AgentHostSkillCompletionProvider extends Disposable implements IAge
* snapshot for the session yet.
*/
private readonly _getHostCustomizations: (session: URI | string) => readonly Customization[] | undefined = () => undefined,
private readonly _resolveSessionCustomizations?: (agent: IAgent, session: URI) => Promise<readonly Customization[]>,
) {
super();
}
Expand Down Expand Up @@ -88,7 +89,9 @@ export class AgentHostSkillCompletionProvider extends Disposable implements IAge

private async _getCandidates(agent: IAgent, session: URI): Promise<readonly SlashCommmandCandidate[]> {
const chat = URI.parse(buildDefaultChatUri(session));
const customizations = await agent.getChatCustomizations(chat, { configurationResource: session, resource: session }, this._getHostCustomizations(session));
const customizations = this._resolveSessionCustomizations
? await this._resolveSessionCustomizations(agent, session)
: await agent.getChatCustomizations(chat, { configurationResource: session, resource: session }, this._getHostCustomizations(session));
const result: SlashCommmandCandidate[] = [];
for (const c of customizations) {
if (c.type === CustomizationType.McpServer || (c.type === CustomizationType.Plugin ? !isCustomizationEnabled(c) : !c.enabled) || !c.children) {
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/agentHost/node/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,7 @@ export class AgentService extends Disposable implements IAgentService {
const provider = this._register(new AgentHostSkillCompletionProvider(
session => this._providerService.getProviderForSession(session),
session => this._hostCustomizations(URI.isUri(session) ? session : URI.parse(session)),
(agent, session) => this._sideEffects.refreshSessionCustomizations(agent, session.toString()),
));
this._register(this._completions.registerProvider(provider));
}
Expand Down
24 changes: 17 additions & 7 deletions src/vs/platform/agentHost/node/agentSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ export class AgentSideEffects extends Disposable {
this._stateManager.dispatchServerAction(ROOT_STATE_URI, { type: ActionType.RootAgentsChanged, agents: infos });
}

private async _publishSessionCustomizations(agent: IAgent, session: ProtocolURI, supersededRetries: number): Promise<void> {
private async _publishSessionCustomizations(agent: IAgent, session: ProtocolURI, supersededRetries: number): Promise<readonly Customization[]> {
const currentBeforeFetch = this._stateManager.getSessionState(session)?.customizations;
const chat = URI.parse(this._stateManager.getSessionState(session)?.defaultChat ?? buildDefaultChatUri(session));
const customizations = await agent.getChatCustomizations(chat, this._chatContext(session, chat.toString()), currentBeforeFetch);
Expand All @@ -473,29 +473,39 @@ export class AgentSideEffects extends Disposable {
if (supersededRetries < MAX_SUPERSEDED_CUSTOMIZATION_PUBLISH_RETRIES) {
this._publishSessionCustomizationsSoon(agent, session, supersededRetries + 1);
}
return;
return current ?? [];
}
if (current && equals(current, customizations)) {
return;
return current;
}

this._stateManager.dispatchServerAction(session, {
type: ActionType.SessionCustomizationsChanged,
customizations: [...customizations],
});
return customizations;
}

private _publishSessionCustomizationsSoon(agent: IAgent, session: ProtocolURI, supersededRetries = 0): void {
void this.refreshSessionCustomizations(agent, session, supersededRetries).catch(() => { });
}

/**
* Resolves and publishes the session's effective customizations in serialization order.
*/
refreshSessionCustomizations(agent: IAgent, session: ProtocolURI, supersededRetries = 0): Promise<readonly Customization[]> {
const previous = this._pendingSessionCustomizationPublishes.get(session) ?? Promise.resolve();
const publish = previous.then(() => this._publishSessionCustomizations(agent, session, supersededRetries)).catch(err => {
const publish = previous.then(() => this._publishSessionCustomizations(agent, session, supersededRetries));
const tracked = publish.then(() => undefined, err => {
this._logService.error('[AgentSideEffects] getChatCustomizations failed', err);
});
this._pendingSessionCustomizationPublishes.set(session, publish);
void publish.finally(() => {
if (this._pendingSessionCustomizationPublishes.get(session) === publish) {
this._pendingSessionCustomizationPublishes.set(session, tracked);
void tracked.finally(() => {
if (this._pendingSessionCustomizationPublishes.get(session) === tracked) {
this._pendingSessionCustomizationPublishes.delete(session);
}
});
return publish;
}

private _publishPendingCustomizationEnablementRefreshes(): void {
Expand Down
35 changes: 35 additions & 0 deletions src/vs/platform/agentHost/test/node/agentService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { SessionDatabase } from '../../node/sessionDatabase.js';
import { ActionType, ActionEnvelope, NotificationType, type INotification } from '../../common/state/sessionActions.js';
import { AH_META_CREATED_BY_SESSION_DB_KEY, AH_META_IS_READ_DB_KEY, AH_META_EHCLI_ADOPTED_DB_KEY, readSessionEhcliAdopted, AH_META_IS_ARCHIVED_DB_KEY, AH_META_WORKSPACELESS_DB_KEY, ChangesetStatus, CustomizationType, MessageAttachmentKind, MessageKind, SessionActiveClient, ResponsePartKind, ROOT_STATE_URI, SESSION_META_FOLDER_PICKER_KEY, SESSION_META_MULTI_ROOT_KEY, SessionLifecycle, SessionSourceControlOutcome, SessionStatus, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, TurnState, buildChatUri, buildDefaultChatUri, buildSubagentChatUri, buildSubagentSessionUri, createErrorResponsePart, customizationId, isDefaultChatUri, isMessageHiddenFromTranscript, isSubagentSession, parseChatUri, parseSubagentSessionUri, readSessionCreationReference, readSessionExternal, readSessionGitHubState, readSessionMultiRootMetadata, readSessionFolderPickerDecision, readSessionSourceControlState, withSessionEhcliAdoptable, withSessionExternal, withSessionMultiRootMetadata, ChatOriginKind, type ChangesetState, type ISessionFolderPickerDecision, type ISessionWithDefaultChat, type MarkdownResponsePart, type SessionState, type SessionSummary, type ToolCallCompletedState, type ToolCallResponsePart, type Turn } from '../../common/state/sessionState.js';
import { ChatInteractivity, type MessageAttachment } from '../../common/state/protocol/state.js';
import { CompletionItemKind } from '../../common/state/protocol/commands.js';
import { isHostSnapshotAttachment, toHostSnapshotAttachmentMeta } from '../../common/meta/agentSnapshotAttachmentMeta.js';
import { readAgentMessageDelegationMeta } from '../../common/meta/agentMessageDelegationMeta.js';
import { IProductService } from '../../../product/common/productService.js';
Expand Down Expand Up @@ -543,6 +544,40 @@ suite('AgentService (node dispatcher)', () => {
}]);
});

test('publishes a newly discovered skill before returning its completion', async () => {
registerTestAgentProvider(service, copilotAgent);
const session = await service.createSession({ provider: 'copilot' });
const customization = {
type: CustomizationType.Plugin,
id: customizationId('file:///dummy'),
uri: 'file:///dummy',
name: 'dummy',
children: [{
type: CustomizationType.Skill,
id: customizationId('file:///dummy/skills/print-hello-world/SKILL.md'),
uri: 'file:///dummy/skills/print-hello-world/SKILL.md',
name: 'print-hello-world',
description: 'Calculate the sum of 3+3 and output it.',
}],
} as const;
copilotAgent.customizations = [customization];

const result = await service.completions({
kind: CompletionItemKind.UserMessage,
channel: session.toString(),
text: '/print',
offset: '/print'.length,
});

assert.deepStrictEqual({
items: result.items.map(item => item.insertText),
published: getStateManager(service).getSessionState(session.toString())?.customizations,
}, {
items: ['/dummy:print-hello-world '],
published: [customization],
});
});

// ---- Provider registration ------------------------------------------

suite('registerProvider', () => {
Expand Down
Loading