Skip to content

Commit e691b3e

Browse files
committed
feat: add default skill templates and update session management to include skill prompts
1 parent 77a779f commit e691b3e

7 files changed

Lines changed: 528 additions & 168 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dist/cli.js",
1818
"templates/tools/**",
1919
"templates/prompts/**",
20+
"templates/skills/**",
2021
"README.md",
2122
"LICENSE"
2223
],

src/prompt.ts

Lines changed: 39 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -8,161 +8,6 @@ import type { SessionMessage } from "./session";
88
import { findGitBashPath, resolveShellPath } from "./common/shell-utils";
99
import { supportsMultimodal } from "./common/model-capabilities";
1010

11-
export const AGENT_DRIFT_GUARD_SKILL = `
12-
---
13-
name: agent-drift-guard
14-
description: Detect and correct execution drift while working on user requests. Use when you are actively implementing, debugging, reviewing, or investigating and there is a risk of wandering beyond the user's goal, adding unrequested work, touching live systems, over-exploring, or ignoring repeated user boundary corrections. Especially useful during multi-step coding tasks, production-adjacent requests, ambiguous scopes, and anytime you should self-check whether it is still solving the requested problem.
15-
---
16-
17-
# Agent Drift Guard
18-
19-
Keep execution tightly aligned with the user's actual request.
20-
21-
## Quick Start
22-
23-
Run this mental check before substantial work and again whenever the plan expands:
24-
25-
1. State the user's requested outcome in one sentence.
26-
2. List explicit non-goals or boundaries the user has set.
27-
3. Ask whether the next action directly advances the requested outcome.
28-
4. If not, either cut it or pause to confirm.
29-
30-
## Drift Signals
31-
32-
Treat these as warning signs that execution may be drifting:
33-
34-
- Exploring broadly before opening the most relevant file, command, or artifact.
35-
- Solving adjacent operational issues when the user asked only for code changes.
36-
- Adding extra safeguards, scripts, docs, refactors, or cleanup that the user did not ask for.
37-
- Reframing the task around what seems "better" instead of what was requested.
38-
- Continuing with a broader plan after the user narrows the scope.
39-
- Repeating searches or tool calls without increasing certainty.
40-
- Mixing diagnosis, remediation, and feature work when the user asked for only one of them.
41-
- Touching production-like state, external systems, or live data without explicit permission.
42-
43-
## Severity Levels
44-
45-
### Level 1: Mild Drift
46-
47-
Examples:
48-
- One or two extra exploratory commands.
49-
- Considering a broader solution but not acting on it yet.
50-
- Briefly over-explaining instead of moving the task forward.
51-
52-
Response:
53-
- Auto-correct silently.
54-
- Narrow to the smallest next action.
55-
- Do not interrupt the user.
56-
57-
### Level 2: Material Drift
58-
59-
Examples:
60-
- Planning additional deliverables not requested.
61-
- Writing helper scripts, migrations, docs, or tests outside the asked scope.
62-
- Expanding from code changes into operational fixes.
63-
- Continuing after the user has already corrected the scope once.
64-
65-
Response:
66-
- Stop and realign internally first.
67-
- If the broader action is avoidable, drop it and continue on scope.
68-
- If the broader action has non-obvious tradeoffs, ask a brief confirmation question.
69-
70-
### Level 3: Boundary or Risk Violation
71-
72-
Examples:
73-
- Modifying live systems, production data, external services, or user-owned state without being asked.
74-
- Taking destructive or hard-to-reverse actions outside the requested scope.
75-
- Ignoring repeated user instructions about what not to do.
76-
77-
Response:
78-
- Pause before acting.
79-
- Surface the exact boundary and ask for confirmation.
80-
- Offer the smallest on-scope option first.
81-
82-
## Self-Check Loop
83-
84-
Use this loop during execution:
85-
86-
### Before the first meaningful action
87-
88-
Write down mentally:
89-
- Requested outcome
90-
- Allowed scope
91-
- Forbidden scope
92-
- Smallest useful next step
93-
94-
### After each non-trivial step
95-
96-
Ask:
97-
- Did this step directly help deliver the requested outcome?
98-
- Did I learn something that changes scope, or only implementation?
99-
- Am I about to do more than the user asked?
100-
101-
### After a user correction
102-
103-
Treat the correction as a hard boundary update.
104-
105-
Then:
106-
- Remove the old broader plan.
107-
- Do not defend the discarded work.
108-
- Continue from the narrowed scope.
109-
- If needed, acknowledge briefly and move on.
110-
111-
## Decision Rules
112-
113-
Use these rules in order:
114-
115-
1. Prefer the most direct artifact first.
116-
- Open the relevant file before scanning the whole repo.
117-
- Inspect the specific failing path before designing a general framework.
118-
119-
2. Prefer the smallest complete fix.
120-
- Solve the asked problem before improving related systems.
121-
- Avoid bonus work unless it is required for correctness.
122-
123-
3. Prefer internal correction over user interruption.
124-
- If you can shrink back to scope confidently, do it.
125-
- Ask only when the next step changes deliverables, risk, or ownership.
126-
127-
4. Treat repeated user constraints as priority signals.
128-
- A repeated instruction means your execution style is currently misaligned.
129-
- Tighten scope immediately.
130-
131-
5. Separate categories of work.
132-
- Code change, investigation, production remediation, cleanup, and documentation are distinct tasks unless the user explicitly combines them.
133-
134-
## Good Intervention Style
135-
136-
When you must pause, keep it short and specific:
137-
138-
- State the potential drift in one sentence.
139-
- Name the tradeoff or boundary.
140-
- Offer the smallest on-scope option first.
141-
142-
Example:
143-
144-
"Quick alignment check: I can keep this to the code fix only, or also add an ops cleanup step. I'll stick to the code fix unless you want both."
145-
146-
## Anti-Patterns
147-
148-
Do not:
149-
150-
- Create cleanup scripts, docs, or side tools just because they seem useful.
151-
- Broaden the task after discovering a neighboring problem.
152-
- Continue with a plan the user has already rejected.
153-
- Justify drift with "best practice" when the user asked for a narrower deliverable.
154-
- Hide extra work inside a larger patch.
155-
156-
## Final Check Before Responding
157-
158-
Before sending the final answer, verify:
159-
160-
- The delivered work matches the requested outcome.
161-
- No extra deliverables were added without confirmation.
162-
- Any assumptions are stated briefly.
163-
- Suggested next steps are optional, not bundled into the completed work.
164-
`;
165-
16611
const COMPACT_PROMPT_BASE = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
16712
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
16813
@@ -254,6 +99,8 @@ type PromptToolOptions = {
25499
webSearchEnabled?: boolean;
255100
};
256101

102+
const DEFAULT_SKILL_TEMPLATES = ["agent-drift-guard.md", "plan-and-execute.md"];
103+
257104
function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): string {
258105
const toolsDir = path.join(extensionRoot, "templates", "tools");
259106
if (!fs.existsSync(toolsDir)) {
@@ -281,17 +128,46 @@ function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): s
281128
return docs.join("\n\n");
282129
}
283130

131+
function readDefaultSkillDocs(extensionRoot: string): Array<{ name: string; content: string }> {
132+
const skillsDir = path.join(extensionRoot, "templates", "skills");
133+
return DEFAULT_SKILL_TEMPLATES.map((entry) => {
134+
const fullPath = path.join(skillsDir, entry);
135+
try {
136+
return {
137+
name: path.basename(entry, ".md"),
138+
content: fs.readFileSync(fullPath, "utf8").trim(),
139+
};
140+
} catch {
141+
return null;
142+
}
143+
}).filter((skill): skill is { name: string; content: string } => Boolean(skill?.content));
144+
}
145+
146+
export function getDefaultSkillPrompt(): string {
147+
const skillDocs = readDefaultSkillDocs(getExtensionRoot());
148+
if (skillDocs.length === 0) {
149+
return "";
150+
}
151+
152+
const blocks = skillDocs.map(
153+
(skill) => `<${skill.name}-skill>
154+
${skill.content}
155+
</${skill.name}-skill>`
156+
);
157+
return `Use the skill documents below to assist the user:\n${blocks.join("\n\n")}`;
158+
}
159+
284160
function getCurrentDateAndModelPrompt(model?: string): string {
285161
const date = new Date();
286162
let prompt = `今天是${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日。随着对话的进行,时间在流逝。`;
287163
prompt += model ? `\n当前LLM模型为${model},对话中可通过/model命令切换模型。` : "";
288164
return prompt;
289165
}
290166

291-
export function getSystemPrompt(projectRoot: string, options: PromptToolOptions = {}): string {
167+
export function getSystemPrompt(_projectRoot: string, options: PromptToolOptions = {}): string {
292168
const toolDocs = readToolDocs(getExtensionRoot(), options);
293169
const basePrompt = toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE;
294-
return `${basePrompt}\n\n${getCurrentDateAndModelPrompt(options.model)}\n\n${getRuntimeContext(projectRoot)}`;
170+
return basePrompt;
295171
}
296172

297173
export function getCompactPrompt(sessionMessages: SessionMessage[]): string {
@@ -310,7 +186,7 @@ export function getCompactPrompt(sessionMessages: SessionMessage[]): string {
310186
return `${COMPACT_PROMPT_BASE}\n\nconversation below:\n\n\`\`\`jsonl\n${jsonl}\n\`\`\``;
311187
}
312188

313-
function getRuntimeContext(projectRoot: string): string {
189+
export function getRuntimeContext(projectRoot: string, model?: string): string {
314190
const uname = getUnameInfo();
315191
const shellPath = getShellPathInfo();
316192
const shellModeOpts = process.platform === "win32" ? { "shell mode": "git-bash" } : {};
@@ -328,7 +204,11 @@ function getRuntimeContext(projectRoot: string): string {
328204
jq: checkToolInstalled("jq"),
329205
},
330206
};
331-
return `# Local Workspace Environment\n\n\`\`\`json
207+
return `${getCurrentDateAndModelPrompt(model)}
208+
209+
# Local Workspace Environment
210+
211+
\`\`\`json
332212
${JSON.stringify(env, null, 2)}
333213
\`\`\``;
334214
}

src/session.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "open
99
import { launchNotifyScript } from "./common/notify";
1010
import { buildThinkingRequestOptions } from "./common/openai-thinking";
1111
import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities";
12-
import { getCompactPrompt, getSystemPrompt, getTools, AGENT_DRIFT_GUARD_SKILL, type ToolDefinition } from "./prompt";
12+
import {
13+
getCompactPrompt,
14+
getDefaultSkillPrompt,
15+
getRuntimeContext,
16+
getSystemPrompt,
17+
getTools,
18+
type ToolDefinition,
19+
} from "./prompt";
1320
import { ToolExecutor, type CreateOpenAIClient } from "./tools/executor";
1421
import { McpManager } from "./mcp/mcp-manager";
1522
import type { McpServerConfig } from "./settings";
@@ -907,20 +914,29 @@ The candidate skills are as follows:\n\n`;
907914
this.saveSessionsIndex(index);
908915
this.removeSessionMessages(droppedEntries.map((item) => item.id));
909916

910-
const systemPrompt = getSystemPrompt(this.projectRoot, this.getPromptToolOptions());
917+
const promptToolOptions = this.getPromptToolOptions();
918+
const systemPrompt = getSystemPrompt(this.projectRoot, promptToolOptions);
911919
const systemMessage = this.buildSystemMessage(sessionId, systemPrompt);
912920
this.appendSessionMessage(sessionId, systemMessage);
913921

922+
const defaultSkillPrompt = getDefaultSkillPrompt();
923+
if (defaultSkillPrompt) {
924+
const defaultSkillMessage = this.buildSystemMessage(sessionId, defaultSkillPrompt);
925+
this.appendSessionMessage(sessionId, defaultSkillMessage);
926+
}
927+
928+
const runtimeContextMessage = this.buildSystemMessage(
929+
sessionId,
930+
getRuntimeContext(this.projectRoot, promptToolOptions.model)
931+
);
932+
this.appendSessionMessage(sessionId, runtimeContextMessage);
933+
914934
const agentInstructions = this.loadAgentInstructions();
915935
if (agentInstructions) {
916936
const instructionsMessage = this.buildSystemMessage(sessionId, agentInstructions);
917937
this.appendSessionMessage(sessionId, instructionsMessage);
918938
}
919939

920-
const defaultSkillPrompt = `Use the skill document below to assist the user:\n<agent-drift-guard-skill>${AGENT_DRIFT_GUARD_SKILL}</agent-drift-guard-skill>`;
921-
const defaultSkillMessage = this.buildSystemMessage(sessionId, defaultSkillPrompt);
922-
this.appendSessionMessage(sessionId, defaultSkillMessage);
923-
924940
const userMessage = this.buildUserMessage(sessionId, userPrompt);
925941
this.appendSessionMessage(sessionId, userMessage);
926942

src/tests/prompt.test.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
33
import * as fs from "fs";
44
import * as path from "path";
55
import { fileURLToPath } from "url";
6-
import { getSystemPrompt, getTools } from "../prompt";
6+
import { getDefaultSkillPrompt, getRuntimeContext, getSystemPrompt, getTools } from "../prompt";
77

88
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
99

@@ -30,11 +30,39 @@ test("getSystemPrompt includes UpdatePlan docs", () => {
3030
assert.equal(prompt.includes("The `plan` argument is a markdown string, not an array of step objects."), true);
3131
});
3232

33-
test("getSystemPrompt includes current date guidance", () => {
33+
test("getSystemPrompt does not include runtime context", () => {
34+
const prompt = getSystemPrompt("/tmp/project");
35+
assert.equal(prompt.includes("# Local Workspace Environment"), false);
36+
assert.equal(prompt.includes('"root path": "/tmp/project"'), false);
37+
});
38+
39+
test("getDefaultSkillPrompt loads default skill templates in order", () => {
40+
const prompt = getDefaultSkillPrompt();
41+
const agentDriftIndex = prompt.indexOf("<agent-drift-guard-skill>");
42+
const planIndex = prompt.indexOf("<plan-and-execute-skill>");
43+
44+
assert.notEqual(agentDriftIndex, -1);
45+
assert.notEqual(planIndex, -1);
46+
assert.equal(agentDriftIndex < planIndex, true);
47+
assert.equal(prompt.includes("Use the skill documents below to assist the user:"), true);
48+
assert.equal(prompt.includes('path="templates/skills/'), false);
49+
});
50+
51+
test("getSystemPrompt does not include current date guidance", () => {
3452
const now = new Date();
3553
const expected = `今天是${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}日。随着对话的进行,时间在流逝。`;
3654
const prompt = getSystemPrompt("/tmp/project");
37-
assert.equal(prompt.includes(expected), true);
55+
assert.equal(prompt.includes(expected), false);
56+
});
57+
58+
test("getRuntimeContext includes current date and model guidance", () => {
59+
const now = new Date();
60+
const expectedDate = `今天是${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}日。随着对话的进行,时间在流逝。`;
61+
const prompt = getRuntimeContext("/tmp/project", "deepseek-v4-pro");
62+
assert.equal(prompt.includes(expectedDate), true);
63+
assert.equal(prompt.includes("当前LLM模型为deepseek-v4-pro,对话中可通过/model命令切换模型。"), true);
64+
assert.equal(prompt.includes("# Local Workspace Environment"), true);
65+
assert.equal(prompt.includes('"root path": "/tmp/project"'), true);
3866
});
3967

4068
test("getSystemPrompt renders Read docs for non-multimodal models", () => {
@@ -47,6 +75,8 @@ test("runtime prompt assets live under templates", () => {
4775
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "web-search.md")), true);
4876
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "read.md.ejs")), true);
4977
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "prompts", "init_command.md.ejs")), true);
78+
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "skills", "agent-drift-guard.md")), true);
79+
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "skills", "plan-and-execute.md")), true);
5080
assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "read.md")), false);
5181
assert.equal(fs.existsSync(path.join(repoRoot, "docs", "tools")), false);
5282
assert.equal(fs.existsSync(path.join(repoRoot, "docs", "prompts")), false);

0 commit comments

Comments
 (0)