mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-31 00:10:04 +08:00
Enable custom agent hooks by default (#311827)
This commit is contained in:
@@ -72,7 +72,7 @@ After creating:
|
||||
|
||||
**Skill vs Custom Agent?** Same capabilities for all steps → Skill. Need context isolation (subagent returns single output) or different tool restrictions per stage → Custom Agent.
|
||||
|
||||
**Hooks vs Instructions?** Instructions *guide* agent behavior (non-deterministic). Hooks *enforce* behavior via shell commands at lifecycle events like `PreToolUse` or `PostToolUse` — they can block operations, require approval, or run formatters deterministically. See [hooks reference](./references/hooks.md).
|
||||
**Hooks vs Instructions?** Instructions *guide* agent behavior (non-deterministic). Hooks *enforce* behavior via shell commands at lifecycle events like `PreToolUse` or `PostToolUse` — they can block operations, require approval, or run formatters deterministically. Hooks can be defined in standalone `.json` files (see [hooks reference](./references/hooks.md)) or inline in custom agent frontmatter via the `hooks` attribute (see [agents reference](./references/agents.md)).
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ agents: [agent1, agent2] # Optional, restrict allowed subagents by name (omi
|
||||
user-invocable: true # Optional, show in agent picker (default: true)
|
||||
disable-model-invocation: false # Optional, prevent subagent invocation (default: false)
|
||||
handoffs: [...] # Optional, transitions to other agents
|
||||
hooks: # Optional, inline hooks for this agent's lifecycle events
|
||||
PreToolUse:
|
||||
- type: command
|
||||
command: "./scripts/validate.sh"
|
||||
PostToolUse:
|
||||
- type: command
|
||||
command: "./scripts/format.sh"
|
||||
---
|
||||
```
|
||||
|
||||
@@ -108,4 +115,31 @@ You are a specialist at {specific task}. Your job is to {clear purpose}.
|
||||
- **Swiss-army agents**: Too many tools, tries to do everything
|
||||
- **Vague descriptions**: "A helpful agent" doesn't guide delegation—be specific
|
||||
- **Role confusion**: Description doesn't match body persona
|
||||
- **Circular handoffs**: A → B → A without progress criteria
|
||||
- **Circular handoffs**: A → B → A without progress criteria
|
||||
|
||||
## Inline Hooks
|
||||
|
||||
Custom agents support inline `hooks` in frontmatter. These hooks execute shell commands at agent lifecycle points and are scoped to this agent only. The format matches standalone hook files (see [hooks reference](../hooks.md)).
|
||||
|
||||
### Supported Events
|
||||
|
||||
`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `SubagentStart`, `SubagentStop`, `Stop`
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: "Secure code reviewer that blocks dangerous commands"
|
||||
tools: [read, search, execute]
|
||||
hooks:
|
||||
PreToolUse:
|
||||
- type: command
|
||||
command: "./scripts/block-dangerous-cmds.sh"
|
||||
timeout: 10
|
||||
PostToolUse:
|
||||
- type: command
|
||||
command: "./scripts/auto-lint.sh"
|
||||
---
|
||||
```
|
||||
|
||||
Each hook command supports: `type` (must be `command`), `command`, platform overrides (`windows`, `linux`, `osx`), `cwd`, `env`, `timeout`.
|
||||
|
||||
@@ -1337,15 +1337,6 @@ configurationRegistry.registerConfiguration({
|
||||
disallowConfigurationDefault: true,
|
||||
tags: ['preview', 'prompts', 'hooks', 'agent']
|
||||
},
|
||||
[PromptsConfig.USE_CUSTOM_AGENT_HOOKS]: {
|
||||
type: 'boolean',
|
||||
title: nls.localize('chat.useCustomAgentHooks.title', "Use Custom Agent Hooks",),
|
||||
markdownDescription: nls.localize('chat.useCustomAgentHooks.description', "Controls whether hooks defined in custom agent frontmatter are parsed and executed. When disabled, hooks from agent files are ignored.",),
|
||||
default: false,
|
||||
restricted: true,
|
||||
disallowConfigurationDefault: true,
|
||||
tags: ['preview', 'prompts', 'hooks', 'agent']
|
||||
},
|
||||
[PromptsConfig.PROMPT_FILES_SUGGEST_KEY]: {
|
||||
type: 'object',
|
||||
scope: ConfigurationScope.RESOURCE,
|
||||
|
||||
@@ -115,11 +115,6 @@ export namespace PromptsConfig {
|
||||
*/
|
||||
export const USE_CLAUDE_HOOKS = 'chat.useClaudeHooks';
|
||||
|
||||
/**
|
||||
* Configuration key for enabling hooks defined in custom agent frontmatter.
|
||||
*/
|
||||
export const USE_CUSTOM_AGENT_HOOKS = 'chat.useCustomAgentHooks';
|
||||
|
||||
/**
|
||||
* Configuration key for enabling stronger skill adherence prompt (experimental).
|
||||
*/
|
||||
|
||||
@@ -22,8 +22,6 @@ import { formatArrayValue, getQuotePreference } from '../utils/promptEditHelper.
|
||||
import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js';
|
||||
import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js';
|
||||
import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { PromptsConfig } from '../config/config.js';
|
||||
|
||||
export class PromptHeaderAutocompletion implements CompletionItemProvider {
|
||||
/**
|
||||
@@ -42,7 +40,6 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
|
||||
@ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService,
|
||||
@IChatModeService private readonly chatModeService: IChatModeService,
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -143,9 +140,6 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider {
|
||||
|
||||
const target = getTarget(promptType, header);
|
||||
const attributesToPropose = new Set(getValidAttributeNames(promptType, false, target));
|
||||
if (!this.configurationService.getValue<boolean>(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) {
|
||||
attributesToPropose.delete(PromptHeaderAttributes.hooks);
|
||||
}
|
||||
for (const attr of header.attributes) {
|
||||
attributesToPropose.delete(attr.key);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import { IHeaderAttribute, ISequenceValue, parseCommaSeparatedList, PromptBody,
|
||||
import { ClaudeHeaderAttributes, getAttributeDefinition, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptFileAttributes.js';
|
||||
import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js';
|
||||
import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { PromptsConfig } from '../config/config.js';
|
||||
|
||||
export class PromptHoverProvider implements HoverProvider {
|
||||
/**
|
||||
@@ -33,7 +31,6 @@ export class PromptHoverProvider implements HoverProvider {
|
||||
@ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService,
|
||||
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
|
||||
@IChatModeService private readonly chatModeService: IChatModeService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -92,9 +89,6 @@ export class PromptHoverProvider implements HoverProvider {
|
||||
case PromptHeaderAttributes.handOffs:
|
||||
return this.getHandsOffHover(attribute, position, target);
|
||||
case PromptHeaderAttributes.hooks:
|
||||
if (!this.configurationService.getValue<boolean>(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) {
|
||||
return undefined;
|
||||
}
|
||||
return this.getHooksHover(attribute, position, description, target);
|
||||
case PromptHeaderAttributes.infer:
|
||||
return this.createHover(description + '\n\n' + localize('promptHeader.attribute.infer.hover', 'Deprecated: Use `user-invocable` and `disable-model-invocation` instead.'), attribute.range);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { isEmptyPattern, parse, splitGlobAware } from '../../../../../../base/co
|
||||
import { Iterable } from '../../../../../../base/common/iterator.js';
|
||||
import { Range } from '../../../../../../editor/common/core/range.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { IMarkerData, MarkerSeverity, MarkerTag } from '../../../../../../platform/markers/common/markers.js';
|
||||
import { ChatMode, IChatMode, IChatModeService } from '../../chatModes.js';
|
||||
import { ChatModeKind } from '../../constants.js';
|
||||
@@ -24,7 +23,6 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js
|
||||
import { dirname } from '../../../../../../base/common/resources.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { HOOKS_BY_TARGET } from '../hookTypes.js';
|
||||
import { PromptsConfig } from '../config/config.js';
|
||||
import { GithubPromptHeaderAttributes } from './promptFileAttributes.js';
|
||||
import { ILogService } from '../../../../../../platform/log/common/log.js';
|
||||
|
||||
@@ -38,7 +36,6 @@ export class PromptValidator {
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IPromptsService private readonly promptsService: IPromptsService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ILogService private readonly logger: ILogService,
|
||||
) { }
|
||||
|
||||
@@ -213,9 +210,7 @@ export class PromptValidator {
|
||||
this.validateUserInvocable(attributes, report);
|
||||
this.validateDisableModelInvocation(attributes, report);
|
||||
this.validateTools(attributes, ChatModeKind.Agent, target, report);
|
||||
if (this.configurationService.getValue<boolean>(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) {
|
||||
this.validateHooks(attributes, target, report);
|
||||
}
|
||||
this.validateHooks(attributes, target, report);
|
||||
if (isVSCodeOrDefaultTarget(target)) {
|
||||
this.validateModel(attributes, ChatModeKind.Agent, report);
|
||||
this.validateHandoffs(attributes, report);
|
||||
@@ -237,19 +232,12 @@ export class PromptValidator {
|
||||
}
|
||||
|
||||
private checkForInvalidArguments(attributes: IHeaderAttribute[], promptType: PromptsType, target: Target, report: (markers: IMarkerData) => void): void {
|
||||
let validAttributeNames = getValidAttributeNames(promptType, true, target);
|
||||
if (!this.configurationService.getValue<boolean>(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)) {
|
||||
validAttributeNames = validAttributeNames.filter(name => name !== PromptHeaderAttributes.hooks);
|
||||
}
|
||||
const useCustomAgentHooks = this.configurationService.getValue<boolean>(PromptsConfig.USE_CUSTOM_AGENT_HOOKS);
|
||||
const validAttributeNames = getValidAttributeNames(promptType, true, target);
|
||||
const validGithubCopilotAttributeNames = new Lazy(() => new Set(getValidAttributeNames(promptType, false, Target.GitHubCopilot)));
|
||||
for (const attribute of attributes) {
|
||||
if (!validAttributeNames.includes(attribute.key)) {
|
||||
const supportedNames = new Lazy(() => {
|
||||
let names = getValidAttributeNames(promptType, false, target);
|
||||
if (!useCustomAgentHooks) {
|
||||
names = names.filter(name => name !== PromptHeaderAttributes.hooks);
|
||||
}
|
||||
const names = getValidAttributeNames(promptType, false, target);
|
||||
return names.sort().join(', ');
|
||||
});
|
||||
switch (promptType) {
|
||||
|
||||
@@ -216,9 +216,10 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
() => Event.any(
|
||||
this.getFileLocatorEvent(PromptsType.agent),
|
||||
Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent),
|
||||
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CHAT_HOOKS)),
|
||||
this._onDidContributedWhenChange.event,
|
||||
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)),
|
||||
this._onDidPluginPromptFilesChange.event,
|
||||
this.workspaceTrustService.onDidChangeTrust,
|
||||
)
|
||||
));
|
||||
|
||||
@@ -772,6 +773,8 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
const stopWatch = StopWatch.create(true);
|
||||
const allAgentFiles = await this.listPromptFiles(PromptsType.agent, token);
|
||||
const disabledAgents = this.getDisabledPromptFiles(PromptsType.agent);
|
||||
const useChatHooks = this.configurationService.getValue(PromptsConfig.USE_CHAT_HOOKS);
|
||||
const isWorkspaceTrusted = this.workspaceTrustService.isWorkspaceTrusted();
|
||||
|
||||
// Get user home for tilde expansion in hook cwd paths
|
||||
const userHomeUri = await this.pathService.userHome();
|
||||
@@ -846,9 +849,8 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
|
||||
// Parse hooks from the frontmatter if present
|
||||
let hooks: ChatRequestHooks | undefined;
|
||||
const useCustomAgentHooks = this.configurationService.getValue<boolean>(PromptsConfig.USE_CUSTOM_AGENT_HOOKS);
|
||||
const hooksRaw = ast.header.hooksRaw;
|
||||
if (useCustomAgentHooks && hooksRaw) {
|
||||
if (useChatHooks && isWorkspaceTrusted && hooksRaw) {
|
||||
const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? defaultFolder;
|
||||
const workspaceRootUri = hookWorkspaceFolder?.uri;
|
||||
hooks = parseSubagentHooksFromYaml(hooksRaw, workspaceRootUri, userHome, target);
|
||||
|
||||
@@ -37,7 +37,6 @@ suite('PromptHeaderAutocompletion', () => {
|
||||
setup(async () => {
|
||||
const testConfigService = new TestConfigurationService();
|
||||
testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true);
|
||||
testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true);
|
||||
instaService = workbenchInstantiationService({
|
||||
contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)),
|
||||
configurationService: () => testConfigService
|
||||
|
||||
@@ -37,7 +37,6 @@ suite('PromptHoverProvider', () => {
|
||||
setup(async () => {
|
||||
const testConfigService = new TestConfigurationService();
|
||||
testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true);
|
||||
testConfigService.setUserConfiguration('chat.useCustomAgentHooks', true);
|
||||
instaService = workbenchInstantiationService({
|
||||
contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)),
|
||||
configurationService: () => testConfigService
|
||||
|
||||
@@ -28,7 +28,6 @@ import { PromptFileParser } from '../../../../common/promptSyntax/promptFilePars
|
||||
import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js';
|
||||
import { MockChatModeService } from '../../../common/mockChatModeService.js';
|
||||
import { MockPromptsService } from '../../../common/promptSyntax/service/mockPromptsService.js';
|
||||
import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js';
|
||||
|
||||
suite('PromptValidator', () => {
|
||||
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -43,7 +42,6 @@ suite('PromptValidator', () => {
|
||||
|
||||
testConfigService = new TestConfigurationService();
|
||||
testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true);
|
||||
testConfigService.setUserConfiguration(PromptsConfig.USE_CUSTOM_AGENT_HOOKS, true);
|
||||
instaService = workbenchInstantiationService({
|
||||
contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)),
|
||||
configurationService: () => testConfigService
|
||||
|
||||
Reference in New Issue
Block a user