Skip to content
Draft
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
4 changes: 4 additions & 0 deletions build/gulpfile.reh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ const serverResourceIncludes = [
'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish',
'out-build/vs/workbench/contrib/terminal/common/scripts/psreadline/**',

// Agent host built-in skills (bundled, harness-owned) - the agent host runs
// on the server for remote sessions, so the SKILL.md files must ship here too.
'out-build/vs/platform/agentHost/node/copilot/skills/**/SKILL.md',

];

const serverResourceExcludes = [
Expand Down
3 changes: 3 additions & 0 deletions build/gulpfile.vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ const vscodeResourceIncludes = [
'out-build/vs/sessions/prompts/*.prompt.md',
'out-build/vs/sessions/skills/**/SKILL.md',

// Agent host built-in skills (bundled, harness-owned)
'out-build/vs/platform/agentHost/node/copilot/skills/**/SKILL.md',

// Extensions
'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}',
'out-build/vs/workbench/services/extensionManagement/common/media/*.{svg,png}',
Expand Down
7 changes: 7 additions & 0 deletions build/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ const desktopResourcePatterns = [
// Sessions - built-in prompts and skills
'vs/sessions/prompts/*.prompt.md',
'vs/sessions/skills/**/SKILL.md',

// Agent host built-in skills (bundled, harness-owned)
'vs/platform/agentHost/node/copilot/skills/**/SKILL.md',
];

// Resources for server target (minimal - no UI)
Expand Down Expand Up @@ -323,6 +326,10 @@ const serverResourcePatterns = [
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.ps1xml',
'vs/workbench/contrib/terminal/common/scripts/psreadline/net6plus/*.dll',
'vs/workbench/contrib/terminal/common/scripts/psreadline/netstd/*.dll',

// Agent host built-in skills (bundled, harness-owned) - the agent host runs
// on the server for remote sessions, so the SKILL.md files must ship here too.
'vs/platform/agentHost/node/copilot/skills/**/SKILL.md',
];

// Resources for server-web target (server + web UI)
Expand Down
25 changes: 25 additions & 0 deletions src/vs/platform/agentHost/common/agentHostSkillInvocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/**
* Builds the message that invokes a bundled skill.
*
* A skill loaded into the harness runtime (via its skill directory) is invoked
* by a message that asks the model to use the `skill` tool to load and follow
* it. So a `/<name>` built-in skill request is rewritten into this canonical
* phrasing - the same contract the Copilot SDK reference client uses.
*
* Harness-agnostic and pure so every harness (and the unit tests) share one
* definition of the invocation contract.
*
* @param skillName The bundled skill's name (its folder / `/<name>` command).
* @param userInstructions Optional free text the user typed after the command,
* forwarded as additional context for the skill.
*/
export function buildSkillInvocationPrompt(skillName: string, userInstructions?: string): string {
const base = `Use the skill tool to invoke the '${skillName}' skill, then follow the skill's instructions.`;
const trimmed = userInstructions?.trim();
return trimmed ? `${base}\n\nAdditional context from the user:\n${trimmed}` : base;
}
105 changes: 105 additions & 0 deletions src/vs/platform/agentHost/common/agentHostTroubleshoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { URI } from '../../../base/common/uri.js';
import { AgentSession } from './agentService.js';
import { buildSkillInvocationPrompt } from './agentHostSkillInvocation.js';
import { readSessionReferenceAttachmentMeta } from './meta/agentSessionReferenceMeta.js';
import type { MessageAttachment } from './state/protocol/state.js';

/**
* The name of the built-in troubleshoot skill and the leading slash-command the
* user types (`/troubleshoot`). The same command name is used across every
* harness (each bundles its own `troubleshoot` skill describing its own logs),
* so the send-path can recognize a troubleshoot request harness-agnostically.
*/
export const TROUBLESHOOT_SKILL_NAME = 'troubleshoot';

/**
* Result of {@link augmentTroubleshootRequest}: the rewritten prompt and the
* attachments to forward to the model.
*/
export interface ITroubleshootRequest {
readonly prompt: string;
readonly attachments: readonly MessageAttachment[] | undefined;
}

/**
* Rewrites a `/troubleshoot` request into a message that invokes the built-in
* `troubleshoot` skill:
*
* Use the skill tool to invoke the 'troubleshoot' skill, then follow the
* skill's instructions.
*
* Session log: <path(s)>
*
* An explicit `#session` reference pins the referenced session's log path (and
* its marker attachments are dropped, consumed here rather than forwarded). A
* bare `/troubleshoot` injects no path, so the skill continues the session
* already established earlier in the conversation (its sticky-reference rule),
* or self-discovers the current session on a first call. The user's genuine
* free text is always forwarded as context, with the inserted `#session:<title>`
* marker text stripped out so only a real question (if any) survives - e.g.
* `/troubleshoot #session:Build why was the test skipped?` keeps "why was the
* test skipped?".
*
* Harness-agnostic: each harness supplies `resolveSessionLogPath`, mapping a raw
* session id to that harness's on-disk log (Copilot CLI's `events.jsonl`,
* Claude's transcript, etc.), keeping this a pure, unit-testable function.
*/
export function augmentTroubleshootRequest(
userInstructions: string,
attachments: readonly MessageAttachment[] | undefined,
resolveSessionLogPath: (sessionId: string) => string,
): ITroubleshootRequest {
const referencedIds: string[] = [];
const remaining: MessageAttachment[] = [];
const markerLabels: string[] = [];
for (const attachment of attachments ?? []) {
const meta = readSessionReferenceAttachmentMeta(attachment._meta);
if (meta) {
// Resolve the raw session id from the resource the same way the
// current session id is derived, so it matches the on-disk session
// folder regardless of the meta's own id.
try {
referencedIds.push(AgentSession.id(URI.parse(meta.sessionResource)));
} catch {
// Ignore an unparseable reference.
}
// Remember the inserted marker text (`#session:<label>`) so it can be
// stripped from the user's free text below.
markerLabels.push(attachment.label);
} else {
remaining.push(attachment);
}
}

const parts = [buildSkillInvocationPrompt(TROUBLESHOOT_SKILL_NAME)];
// Pin the log path only for an explicit `#session` reference. A bare
// `/troubleshoot` injects no path so the skill continues the session already
// established earlier in the conversation, or self-discovers the current one.
if (referencedIds.length) {
const logPaths = Array.from(new Set(referencedIds.map(resolveSessionLogPath)));
if (logPaths.length) {
parts.push(`Session log: ${logPaths.join(', ')}`);
}
}
// Forward the user's genuine free text, stripping the inserted
// `#session:<title>` marker(s) - their target is conveyed via the log path
// above, so what remains is any real question the user typed alongside the
// reference (e.g. "#session:Build why was the test skipped?").
let instructions = userInstructions;
for (const label of markerLabels) {
instructions = instructions.replace(`#session:${label}`, ' ');
}
instructions = instructions.trim();
if (instructions) {
parts.push(`Additional context from the user:\n${instructions}`);
}

// Drop the `#session` markers; forward any other (real) attachments.
const nextAttachments = referencedIds.length ? remaining : attachments;
return { prompt: parts.join('\n\n'), attachments: nextAttachments };
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import type { SimpleMessageAttachment } from '../state/protocol/state.js';
import { readSessionReferenceAttachmentMeta } from './agentSessionReferenceMeta.js';

/**
* Well-known typed views over a `SimpleMessageAttachment`'s `_meta` bag as
Expand Down Expand Up @@ -44,6 +45,27 @@ export interface ISkillCompletionAttachmentMeta {
readonly description?: string;
}

/**
* Top-level `_meta` key carrying a display-only, human-readable date for a
* `#session` completion (e.g. last-modified time), shown as the completion's
* description. Kept separate from the session-reference contract so it does not
* affect send-time resolution.
*/
export const SESSION_REFERENCE_DISPLAY_DATE_META_KEY = 'sessionReferenceDisplayDate';

/**
* The `_meta` shape attached to a `completions` result that resolves to a
* `#session` reference (another agent session to attach as context).
*/
export interface ISessionReferenceCompletionAttachmentMeta {
/** The referenced session's resource URI as a string. */
readonly sessionResource: string;
/** The referenced session's id. */
readonly sessionID: string;
/** Optional human-readable date shown as the completion's description. */
readonly date?: string;
}

/**
* A typed, discriminated view over the well-known `completions` attachment
* `_meta` variants. The `kind` discriminant is computed by
Expand All @@ -52,7 +74,8 @@ export interface ISkillCompletionAttachmentMeta {
*/
export type CompletionAttachmentMeta =
| ({ readonly kind: 'command' } & ICommandCompletionAttachmentMeta)
| ({ readonly kind: 'skill' } & ISkillCompletionAttachmentMeta);
| ({ readonly kind: 'skill' } & ISkillCompletionAttachmentMeta)
| ({ readonly kind: 'sessionReference' } & ISessionReferenceCompletionAttachmentMeta);

/**
* Reads the well-known `completions` attachment `_meta` keys, classifying the
Expand All @@ -65,6 +88,18 @@ export function readCompletionAttachmentMeta(attachment: SimpleMessageAttachment
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
return undefined;
}
// A `#session` reference carries its data under a dedicated nested key rather
// than a flat discriminator, so classify it first.
const sessionRef = readSessionReferenceAttachmentMeta(meta);
if (sessionRef) {
const date = meta[SESSION_REFERENCE_DISPLAY_DATE_META_KEY];
return {
kind: 'sessionReference',
sessionResource: sessionRef.sessionResource,
sessionID: sessionRef.sessionID,
...(typeof date === 'string' ? { date } : {}),
};
}
if (typeof meta['command'] === 'string') {
return {
kind: 'command',
Expand Down
61 changes: 61 additions & 0 deletions src/vs/platform/agentHost/common/meta/agentSessionReferenceMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/**
* Well-known `_meta` key carried by a `#session` reference message attachment.
* A session reference lets the user point the built-in `/troubleshoot` skill at
* a session **other than** the current one; the harness reads this meta at send
* time to resolve the referenced session's on-disk event log.
*
* Kept in the platform layer so both the harness (which produces the `#session`
* completion and consumes it at send time) and the workbench (which renders the
* attachment pill and restores it on reload) share one contract.
*/
export const AgentHostSessionReferenceAttachmentMetadataKey = 'vscode.agentHost.sessionReference';

/**
* Typed view over the {@link AgentHostSessionReferenceAttachmentMetadataKey}
* payload on a session-reference attachment's `_meta` bag.
*/
export interface IAgentHostSessionReferenceAttachmentMetadata {
/** The referenced session's resource URI, serialized via `URI.toString()`. */
readonly sessionResource: string;
/** The referenced session's id (the `chatSessionResourceToId` of the resource). */
readonly sessionID: string;
}

/**
* Builds the `_meta` bag for a session-reference attachment. Producers (the
* `#session` completion provider) MUST go through this so the shape stays in
* lock-step with {@link readSessionReferenceAttachmentMeta}.
*/
export function toSessionReferenceAttachmentMeta(metadata: IAgentHostSessionReferenceAttachmentMetadata): Record<string, unknown> {
return {
[AgentHostSessionReferenceAttachmentMetadataKey]: {
sessionResource: metadata.sessionResource,
sessionID: metadata.sessionID,
} satisfies IAgentHostSessionReferenceAttachmentMetadata,
};
}

/**
* Reads the {@link IAgentHostSessionReferenceAttachmentMetadata} from an
* attachment's `_meta` bag, returning `undefined` when the bag is missing or
* malformed.
*/
export function readSessionReferenceAttachmentMeta(meta: Record<string, unknown> | undefined): IAgentHostSessionReferenceAttachmentMetadata | undefined {
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
return undefined;
}
const raw = meta[AgentHostSessionReferenceAttachmentMetadataKey];
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return undefined;
}
const typed = raw as Partial<IAgentHostSessionReferenceAttachmentMetadata>;
if (typeof typed.sessionResource !== 'string' || typeof typed.sessionID !== 'string') {
return undefined;
}
return { sessionResource: typed.sessionResource, sessionID: typed.sessionID };
}
65 changes: 65 additions & 0 deletions src/vs/platform/agentHost/node/agentHostCompletionTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CompletionTriggerCharacter } from './agentHostCompletions.js';

/** Token the user types after `#` to reference another session. */
export const SESSION_TOKEN = 'session';

/**
* Result of {@link extractAtToken}.
*/
export interface IAtToken {
readonly token: string;
readonly triggerChar: string;
readonly rangeStart: number;
readonly rangeEnd: number;
}

/**
* Walk back from `offset` to find the most recent `@`/`#` that is preceded by
* whitespace (or start-of-string) and not interrupted by whitespace. Returns
* the substring after the trigger together with the range to replace, or
* `undefined` if no such token is being typed at `offset`.
*
* Shared by the file- and session-reference completion providers (and exported
* for unit testing) so it lives in a neutral module rather than either provider.
*/
export function extractAtToken(text: string, offset: number): IAtToken | undefined {
if (offset < 0 || offset > text.length) {
return undefined;
}
for (let i = offset - 1; i >= 0; i--) {
const ch = text.charCodeAt(i);
// whitespace terminates the search
if (ch === 0x20 /* space */ || ch === 0x09 /* tab */ || ch === 0x0a /* \n */ || ch === 0x0d /* \r */) {
return undefined;
}
if (text[i] === CompletionTriggerCharacter.File || text[i] === CompletionTriggerCharacter.Hash) {
// The trigger character must be at start-of-input or preceded by whitespace.
if (i > 0) {
const prev = text.charCodeAt(i - 1);
const prevIsWs = prev === 0x20 || prev === 0x09 || prev === 0x0a || prev === 0x0d;
if (!prevIsWs) {
return undefined;
}
}
return { token: text.slice(i + 1, offset), triggerChar: text[i], rangeStart: i, rangeEnd: offset };
}
}
return undefined;
}

/**
* Whether a `#`-token is heading toward a `#session` reference - a non-empty
* prefix of `session` (so it matches as soon as `#s` is typed) or the completed
* `session:<filter>` form. Deliberately does NOT match tokens that merely start
* with `session` (e.g. `#sessions`, `#sessionManager`), so the file-reference
* provider only cedes genuine session references. Shared so both providers agree.
*/
export function isSessionReferenceToken(token: string): boolean {
const typed = token.toLowerCase();
return typed.length > 0 && (SESSION_TOKEN.startsWith(typed) || typed.startsWith(`${SESSION_TOKEN}:`));
}
Loading
Loading