mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-24 08:37:14 +01:00
Clarify artifact commit guidance Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: benibenj <44439583+benibenj@users.noreply.github.com>
176 lines
8.1 KiB
TypeScript
176 lines
8.1 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { generateUuid } from '../../../../base/common/uuid.js';
|
|
import { ArtifactServerToolName } from '../../common/serverToolNames.js';
|
|
import { parseSessionArtifactInput, SessionArtifactCollection } from '../../common/sessionArtifactCollection.js';
|
|
import { readSessionArtifacts, SESSION_ARTIFACT_TYPES, SessionArtifactType, withSessionArtifacts, type ISessionArtifact } from '../../common/sessionArtifacts.js';
|
|
import { parseRequiredSessionUriFromChatUri, type ToolDefinition } from '../../common/state/sessionState.js';
|
|
import type { AgentHostStateManager } from '../agentHostStateManager.js';
|
|
import type { IServerToolDisplay, IServerToolExecutionContext, IServerToolGroup } from './agentServerToolHost.js';
|
|
|
|
const addArtifactInputSchema: ToolDefinition['inputSchema'] = {
|
|
type: 'object',
|
|
properties: {
|
|
type: {
|
|
type: 'string',
|
|
enum: [...SESSION_ARTIFACT_TYPES],
|
|
description: 'The kind of artifact. Use `resource` only when no other kind applies.',
|
|
},
|
|
label: { type: 'string', description: 'Short label shown to the user.' },
|
|
link: { type: 'string', description: 'URL of the pull request, issue, commit or website. Required for those kinds.' },
|
|
uri: { type: 'string', description: 'URI of the file or resource. Required for the `file` and `resource` kinds.' },
|
|
commitHash: { type: 'string', description: 'The commit hash. Required for the `commit` kind.' },
|
|
createdByThisSession: { type: 'boolean', description: 'Required for the `pullRequest` kind: `true` when this session created the pull request, `false` when it only references an existing one.' },
|
|
},
|
|
required: ['type', 'label'],
|
|
};
|
|
|
|
const removeArtifactInputSchema: ToolDefinition['inputSchema'] = {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string', description: 'The artifact id returned by `add_artifact` or `list_artifacts`.' },
|
|
},
|
|
required: ['id'],
|
|
};
|
|
|
|
const listArtifactsInputSchema: ToolDefinition['inputSchema'] = {
|
|
type: 'object',
|
|
properties: {},
|
|
};
|
|
|
|
export const artifactServerToolDefinitions: ToolDefinition[] = [
|
|
{
|
|
name: ArtifactServerToolName.AddArtifact,
|
|
title: 'Add Artifact',
|
|
description: 'Record something the user will want to open — a pull request, issue, commit found while investigating or answering a question, website, file or other resource — so it is surfaced next to the chat input. Do not record commits you create unless the user explicitly asks you to add them as artifacts.',
|
|
inputSchema: addArtifactInputSchema,
|
|
annotations: { readOnlyHint: false },
|
|
},
|
|
{
|
|
name: ArtifactServerToolName.RemoveArtifact,
|
|
title: 'Remove Artifact',
|
|
description: 'Remove an artifact from this session by id.',
|
|
inputSchema: removeArtifactInputSchema,
|
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
},
|
|
{
|
|
name: ArtifactServerToolName.ListArtifacts,
|
|
title: 'List Artifacts',
|
|
description: 'List the artifacts recorded on this session, with their ids.',
|
|
inputSchema: listArtifactsInputSchema,
|
|
annotations: { readOnlyHint: true },
|
|
},
|
|
];
|
|
|
|
/** Host services the artifact tools need beyond the session state. */
|
|
export interface IArtifactServerToolAccessor {
|
|
/** Whether the artifact tools are advertised and executable. */
|
|
readonly isEnabled: () => boolean;
|
|
/** Persists a session's artifacts so they survive a host restart. */
|
|
readonly persist: (session: string, artifacts: readonly ISessionArtifact[]) => void;
|
|
}
|
|
|
|
function describeArtifact(artifact: ISessionArtifact): string {
|
|
const value = artifact.link ?? artifact.uri ?? artifact.commitHash ?? '';
|
|
return `${artifact.id} (${artifact.type}) ${artifact.label}${value ? ` — ${value}` : ''}`;
|
|
}
|
|
|
|
/**
|
|
* Reads, mutates and republishes the artifacts of the session that owns the
|
|
* executing chat. The artifacts live on the session's `_meta` bag, so a change
|
|
* reaches subscribed clients through the regular action envelope.
|
|
*/
|
|
class SessionArtifacts {
|
|
|
|
private readonly _session: string;
|
|
|
|
constructor(
|
|
private readonly _stateManager: AgentHostStateManager,
|
|
context: IServerToolExecutionContext,
|
|
) {
|
|
this._session = parseRequiredSessionUriFromChatUri(context.chatUri);
|
|
}
|
|
|
|
read(): SessionArtifactCollection {
|
|
return new SessionArtifactCollection(readSessionArtifacts(this._stateManager.getSessionState(this._session)?._meta));
|
|
}
|
|
|
|
write(artifacts: readonly ISessionArtifact[], accessor: IArtifactServerToolAccessor): void {
|
|
const meta = this._stateManager.getSessionState(this._session)?._meta;
|
|
this._stateManager.setSessionMeta(this._session, withSessionArtifacts(meta, artifacts));
|
|
accessor.persist(this._session, artifacts);
|
|
}
|
|
}
|
|
|
|
export function createArtifactServerToolGroup(accessor?: IArtifactServerToolAccessor): IServerToolGroup {
|
|
return {
|
|
definitions: artifactServerToolDefinitions,
|
|
isEnabled(): boolean {
|
|
return accessor?.isEnabled() === true;
|
|
},
|
|
getDisplay(toolName, args): IServerToolDisplay | undefined {
|
|
switch (toolName) {
|
|
case ArtifactServerToolName.AddArtifact: {
|
|
const label = (args as { label?: unknown } | undefined)?.label;
|
|
return typeof label === 'string' && label.length > 0
|
|
? { displayName: 'Add Artifact', invocationMessage: `Add artifact "${label}"`, pastTenseMessage: `Added artifact "${label}"` }
|
|
: { displayName: 'Add Artifact', invocationMessage: 'Add artifact', pastTenseMessage: 'Added artifact' };
|
|
}
|
|
case ArtifactServerToolName.RemoveArtifact:
|
|
return { displayName: 'Remove Artifact', invocationMessage: 'Remove artifact', pastTenseMessage: 'Removed artifact' };
|
|
case ArtifactServerToolName.ListArtifacts:
|
|
return { displayName: 'List Artifacts', invocationMessage: 'List artifacts', pastTenseMessage: 'Listed artifacts' };
|
|
default:
|
|
return undefined;
|
|
}
|
|
},
|
|
execute(stateManager, context, toolName, rawArgs): string {
|
|
if (!accessor) {
|
|
throw new Error(`${toolName} is unavailable in this host.`);
|
|
}
|
|
|
|
const artifacts = new SessionArtifacts(stateManager, context);
|
|
switch (toolName) {
|
|
case ArtifactServerToolName.AddArtifact: {
|
|
const input = parseSessionArtifactInput(rawArgs, ArtifactServerToolName.AddArtifact);
|
|
const result = artifacts.read().add(input, generateUuid);
|
|
if (!result.added) {
|
|
return `Artifact already recorded: ${describeArtifact(result.artifact)}`;
|
|
}
|
|
artifacts.write(result.artifacts, accessor);
|
|
return `Added artifact: ${describeArtifact(result.artifact)}`;
|
|
}
|
|
case ArtifactServerToolName.RemoveArtifact: {
|
|
const id = (rawArgs as { id?: unknown } | undefined)?.id;
|
|
if (typeof id !== 'string' || id.length === 0) {
|
|
throw new Error(`Invalid ${ArtifactServerToolName.RemoveArtifact} input: id must be a non-empty string.`);
|
|
}
|
|
const result = artifacts.read().remove(id);
|
|
if (!result.removed) {
|
|
return `No artifact with id ${id}.`;
|
|
}
|
|
artifacts.write(result.artifacts, accessor);
|
|
return `Removed artifact: ${describeArtifact(result.removed)}`;
|
|
}
|
|
case ArtifactServerToolName.ListArtifacts: {
|
|
const current = artifacts.read().artifacts;
|
|
return current.length === 0
|
|
? 'No artifacts recorded for this session.'
|
|
: current.map(describeArtifact).join('\n');
|
|
}
|
|
default:
|
|
throw new Error(`Unknown artifact tool: ${toolName}`);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The instruction appended to every agent's host instructions while the
|
|
* artifact tools are enabled.
|
|
*/
|
|
export const ARTIFACT_TOOLS_INSTRUCTION = `When you produce something the user will want to open — a pull request, an issue, a website, a plan file or another resource — or find a notable commit worth showing the user while investigating or answering a question, record it once with \`${ArtifactServerToolName.AddArtifact}\` (types: ${SESSION_ARTIFACT_TYPES.join(', ')}; use \`${SessionArtifactType.Resource}\` when nothing else fits). Do not record routine files you merely edited. Do not record commits you create unless the user explicitly asks you to add them as artifacts.`;
|