mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
feat: /goal命令能力支持,参考codex实现 (#1261)
* feat: /goal命令能力支持,参考codex实现 * fix: 修复promp和提示词不一致的问题 * fix: 修复 goal 功能多项 AI 审查问题 - prompt 中 update 行为描述与运行时不一致(no-op → error) - src/commands/goal/ 使用相对路径导入,改为 src/* 别名 - /goal 命令标记 bridgeSafe 但含交互式对话框,改为 false - useGoalContinuation 中 origin 使用 as unknown as string 强转,改为直接传字符串 - ResumeConversation 路径缺少 goal hydration,补齐恢复逻辑 - onCancel 在非查询状态下误暂停 goal,加 queryGuard 守卫 - resumeGoal 允许从终态恢复,收紧为仅允许 paused 状态 - buildGoalContextBlock 生成畸形 XML 属性,改为合法 budget 属性 * fix: 修复剩余AI审查的问题 * fix: 防止goal状态丢失 * fix: 修复Biome规范错误问题 * fix: 修复部分情况下goal无法启动的问题 * fix: 增加断网后状态默认设置为PAUSE机制、完成暂停-恢复状态切换,且正常进行前端渲染。设置达到max turn后处理逻辑。 * fix: 修复终端异常断开情况,resume续跑;修复用户消息排队信息被goal输出信息覆盖的问题。 * fix: apply biome formatting to pass CI lint check Co-authored-by: Cursor <cursoragent@cursor.com> * fix: skip slash command echo in setUserInputOnProcessing to prevent UI flash Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: moyu <moyu@kingsoft.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,7 @@ export { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
|
||||
export { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
|
||||
export { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
|
||||
export { GlobTool } from './tools/GlobTool/GlobTool.js'
|
||||
export { GoalTool } from './tools/GoalTool/GoalTool.js'
|
||||
export { GrepTool } from './tools/GrepTool/GrepTool.js'
|
||||
export { LSPTool } from './tools/LSPTool/LSPTool.js'
|
||||
export { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
|
||||
|
||||
253
packages/builtin-tools/src/tools/GoalTool/GoalTool.ts
Normal file
253
packages/builtin-tools/src/tools/GoalTool/GoalTool.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { z } from 'zod/v4'
|
||||
import { buildTool, type ToolDef } from 'src/Tool.js'
|
||||
import { jsonStringify } from 'src/utils/slowOperations.js'
|
||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||
import {
|
||||
completeGoal,
|
||||
formatGoalElapsed,
|
||||
formatGoalStatusLabel,
|
||||
getGoal,
|
||||
recordBlockedAttempt,
|
||||
} from 'src/services/goal/goalState.js'
|
||||
import { persistCurrentGoal } from 'src/services/goal/goalStorage.js'
|
||||
import { GOAL_TOOL_NAME } from './constants.js'
|
||||
import { DESCRIPTION, generatePrompt } from './prompt.js'
|
||||
|
||||
function toolLog(msg: string): void {
|
||||
try {
|
||||
const { logForDebugging } =
|
||||
require('src/utils/debug.js') as typeof import('src/utils/debug.js')
|
||||
logForDebugging(`[goal] tool: ${msg}`)
|
||||
} catch {
|
||||
/* debug not available */
|
||||
}
|
||||
}
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
action: z
|
||||
.enum(['get', 'update'])
|
||||
.optional()
|
||||
.describe(
|
||||
'Action to perform: "get" to read status, "update" to mark complete or blocked. Defaults to "update" if status is provided, otherwise "get".',
|
||||
),
|
||||
status: z
|
||||
.enum(['complete', 'blocked'])
|
||||
.optional()
|
||||
.describe(
|
||||
'Required for "update". Only "complete" or "blocked" are accepted.',
|
||||
),
|
||||
reason: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Explanation for the status change. Required for "update".'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
success: z.boolean(),
|
||||
goal: z
|
||||
.object({
|
||||
objective: z.string(),
|
||||
status: z.string(),
|
||||
tokensUsed: z.number(),
|
||||
tokenBudget: z.number().nullable(),
|
||||
elapsed: z.string(),
|
||||
turnsExecuted: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
message: z.string().optional(),
|
||||
report: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
export type Input = z.infer<InputSchema>
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
function buildGoalSnapshot() {
|
||||
const goal = getGoal()
|
||||
if (!goal) return undefined
|
||||
return {
|
||||
objective: goal.objective,
|
||||
status: formatGoalStatusLabel(goal.status),
|
||||
tokensUsed: goal.tokensUsed,
|
||||
tokenBudget: goal.tokenBudget,
|
||||
elapsed: formatGoalElapsed(goal),
|
||||
turnsExecuted: goal.turnsExecuted,
|
||||
}
|
||||
}
|
||||
|
||||
function buildCompletionReport(): string {
|
||||
const goal = getGoal()
|
||||
if (!goal) return ''
|
||||
const budget =
|
||||
goal.tokenBudget !== null
|
||||
? `Token usage: ${goal.tokensUsed} / ${goal.tokenBudget}`
|
||||
: `Token usage: ${goal.tokensUsed}`
|
||||
return [
|
||||
'Goal achieved — usage report:',
|
||||
` ${budget}`,
|
||||
` Active time: ${formatGoalElapsed(goal)}`,
|
||||
` Continuation turns: ${goal.turnsExecuted}`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export const GoalTool = buildTool({
|
||||
name: GOAL_TOOL_NAME,
|
||||
searchHint: 'get or update the active goal (complete/blocked)',
|
||||
maxResultSizeChars: 10_000,
|
||||
async description() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
async prompt() {
|
||||
return generatePrompt()
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
userFacingName() {
|
||||
return 'Goal'
|
||||
},
|
||||
shouldDefer: true,
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly(input: Input) {
|
||||
const action = input.action ?? (input.status ? 'update' : 'get')
|
||||
return action === 'get'
|
||||
},
|
||||
toAutoClassifierInput(input: Input) {
|
||||
const action = input.action ?? (input.status ? 'update' : 'get')
|
||||
if (action === 'get') return 'get goal status'
|
||||
return `update goal: ${input.status} — ${input.reason ?? ''}`
|
||||
},
|
||||
async checkPermissions(input: Input) {
|
||||
return { behavior: 'allow' as const, updatedInput: input }
|
||||
},
|
||||
renderToolUseMessage(input: Input) {
|
||||
const action = input.action ?? (input.status ? 'update' : 'get')
|
||||
if (action === 'get') return 'Checking goal status…'
|
||||
return `Updating goal: ${input.status}${input.reason ? ` — ${input.reason}` : ''}`
|
||||
},
|
||||
renderToolResultMessage(output: Output) {
|
||||
if (output.error) return `Goal error: ${output.error}`
|
||||
if (output.report) return output.report
|
||||
if (output.goal) {
|
||||
return `Goal "${output.goal.objective}" — ${output.goal.status}`
|
||||
}
|
||||
return output.message ?? 'Done'
|
||||
},
|
||||
renderToolUseRejectedMessage() {
|
||||
return 'Goal operation rejected'
|
||||
},
|
||||
async call(input: Input): Promise<{ data: Output }> {
|
||||
const action = input.action ?? (input.status ? 'update' : 'get')
|
||||
toolLog(
|
||||
`called: action=${action}${input.status ? ` status=${input.status}` : ''}${input.reason ? ` reason="${input.reason.slice(0, 60)}"` : ''}`,
|
||||
)
|
||||
if (action === 'get') {
|
||||
const snapshot = buildGoalSnapshot()
|
||||
if (!snapshot) {
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
message:
|
||||
'No active goal. The user can set one with `/goal <objective>`.',
|
||||
},
|
||||
}
|
||||
}
|
||||
return { data: { success: true, goal: snapshot } }
|
||||
}
|
||||
|
||||
// action === 'update'
|
||||
if (!input.status) {
|
||||
return {
|
||||
data: {
|
||||
success: false,
|
||||
error:
|
||||
'The "status" field is required for update. Use "complete" or "blocked".',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const goal = getGoal()
|
||||
if (!goal) {
|
||||
return {
|
||||
data: {
|
||||
success: false,
|
||||
error: 'No active goal to update.',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (input.status === 'complete') {
|
||||
const report = buildCompletionReport()
|
||||
completeGoal()
|
||||
persistCurrentGoal()
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
goal: buildGoalSnapshot(),
|
||||
report,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// status === 'blocked'
|
||||
const reason = input.reason ?? 'unspecified blocker'
|
||||
const result = recordBlockedAttempt(reason)
|
||||
if (!result) {
|
||||
return {
|
||||
data: {
|
||||
success: false,
|
||||
error: 'Goal is not in a state that accepts blocked attempts.',
|
||||
},
|
||||
}
|
||||
}
|
||||
persistCurrentGoal()
|
||||
|
||||
if (result.status === 'blocked') {
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
goal: buildGoalSnapshot(),
|
||||
message: `Goal marked as blocked after ${result.attempts} consecutive attempts. Reason: ${reason}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
goal: buildGoalSnapshot(),
|
||||
message: `Blocked attempt ${result.attempts} recorded. The goal remains active — the same condition must persist for 3 consecutive turns before it is marked blocked.`,
|
||||
},
|
||||
}
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(content: Output, toolUseID: string) {
|
||||
if (content.error) {
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result' as const,
|
||||
content: `Error: ${content.error}`,
|
||||
is_error: true,
|
||||
}
|
||||
}
|
||||
const parts: string[] = []
|
||||
if (content.message) parts.push(content.message)
|
||||
if (content.report) parts.push(content.report)
|
||||
if (content.goal) parts.push(jsonStringify(content.goal))
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result' as const,
|
||||
content: parts.join('\n') || 'Done',
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
1
packages/builtin-tools/src/tools/GoalTool/constants.ts
Normal file
1
packages/builtin-tools/src/tools/GoalTool/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const GOAL_TOOL_NAME = 'GoalTool'
|
||||
38
packages/builtin-tools/src/tools/GoalTool/prompt.ts
Normal file
38
packages/builtin-tools/src/tools/GoalTool/prompt.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export const DESCRIPTION =
|
||||
'Get or update the active goal status. The model may only mark a goal as "complete" or "blocked".'
|
||||
|
||||
export function generatePrompt(): string {
|
||||
return `Use this tool to interact with the active thread goal.
|
||||
|
||||
## Actions
|
||||
|
||||
### get
|
||||
Returns the current goal state (objective, status, token usage, elapsed time, turns executed).
|
||||
No input required beyond \`action: "get"\`.
|
||||
|
||||
### update
|
||||
Transition the goal to a terminal status. Only two values are accepted:
|
||||
- **complete** — All requirements are verified (see Completion Audit below).
|
||||
- **blocked** — An insurmountable obstacle has persisted for 3+ consecutive turns (see Blocked Audit below).
|
||||
|
||||
When marking complete, provide a brief \`reason\` summarising what was achieved.
|
||||
When marking blocked, provide a \`reason\` describing the specific blocker.
|
||||
|
||||
## Completion Audit (required before marking complete)
|
||||
1. Derive concrete requirements from the objective.
|
||||
2. Preserve the original scope — do not redefine success around existing work.
|
||||
3. For every requirement, identify authoritative evidence (test output, file content, command result).
|
||||
4. Treat tests and manifests as evidence only after confirming they cover the requirement.
|
||||
5. Treat uncertain or indirect evidence as "not achieved".
|
||||
6. The audit must PROVE completion, not merely fail to find remaining work.
|
||||
|
||||
## Blocked Audit (required before marking blocked)
|
||||
1. The same blocking condition must persist across at least 3 consecutive continuation turns.
|
||||
2. "Difficult", "slow", or "partially incomplete" is NOT blocked.
|
||||
3. Only genuinely insurmountable obstacles qualify (missing credentials, external service down, etc.).
|
||||
|
||||
## Important
|
||||
- You cannot pause, resume, or clear a goal — only the user can do that via \`/goal\`.
|
||||
- If no goal is active, \`get\` returns a message saying so; \`update\` returns an error.
|
||||
- On completion, the tool result includes a usage report (tokens, time, turns).`
|
||||
}
|
||||
Reference in New Issue
Block a user