程序员阿江(Relakkes) 1e32942eab feat: enable autonomous goal loops from slash commands
Coding sessions need a native way to keep working until a stated completion condition is actually met. This adds a CLI /goal command, goal state and evaluator logic, and a headless-safe command filter so interactive and print-mode runs can both start a goal and continue when the evaluator says more work remains.

Constraint: The evaluator must work with saved third-party provider runtimes that may reject structured output schemas.
Constraint: Completion evidence must come from visible transcript text, not hidden goal prompts or assistant thinking blocks.
Rejected: Implement /goal as a bundled skill only | it would not own the query loop or support native continuation semantics.
Rejected: Expose all local-jsx commands to headless mode | interactive UI commands would become available in print mode.
Confidence: high
Scope-risk: moderate
Directive: Keep /goal evaluator evidence scoped to visible transcript text before adding richer completion signals.
Tested: bun test src/commands/headless.test.ts src/goals/goalState.test.ts src/goals/goalEvaluator.test.ts
Tested: bun run check:server
Tested: bun run verify
Tested: Real provider /goal smoke with MiniMax-M2.7-highspeed, including a two-turn continuation that logged Goal continuation #1 and completed with GOAL_SMOKE_LOOP_DONE.
Not-tested: Durable cross-process goal persistence for resumed sessions.
2026-05-13 18:08:46 +08:00

73 lines
2.1 KiB
TypeScript

import * as React from 'react'
import { getSessionId } from '../../bootstrap/state.js'
import type { LocalJSXCommandContext } from '../../commands.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'
import {
buildGoalStartPrompt,
clearThreadGoal,
formatGoalStatus,
getThreadGoal,
parseGoalCommand,
setThreadGoal,
updateThreadGoalStatus,
} from '../../goals/goalState.js'
export async function call(
onDone: LocalJSXCommandOnDone,
_context: LocalJSXCommandContext,
args: string,
): Promise<React.ReactNode> {
const threadId = getSessionId()
try {
const parsed = parseGoalCommand(args)
if (parsed.type === 'status') {
onDone(formatGoalStatus(getThreadGoal(threadId)), { display: 'system' })
return null
}
if (parsed.type === 'clear') {
const cleared = clearThreadGoal(threadId)
onDone(cleared ? 'Goal cleared.' : 'No active goal.', { display: 'system' })
return null
}
if (parsed.type === 'pause') {
const goal = updateThreadGoalStatus(threadId, 'paused')
onDone(goal ? formatGoalStatus(goal) : 'No active goal.', {
display: 'system',
})
return null
}
if (parsed.type === 'resume') {
const goal = updateThreadGoalStatus(threadId, 'active')
onDone(goal ? formatGoalStatus(goal) : 'No goal to resume.', {
display: 'system',
shouldQuery: Boolean(goal),
metaMessages: goal ? [buildGoalStartPrompt(goal)] : [],
})
return null
}
if (parsed.type === 'complete') {
const goal = updateThreadGoalStatus(threadId, 'complete')
onDone(goal ? 'Goal marked complete.' : 'No active goal.', {
display: 'system',
})
return null
}
const goal = setThreadGoal(threadId, {
objective: parsed.objective,
tokenBudget: parsed.tokenBudget,
})
onDone(formatGoalStatus(goal), {
display: 'system',
shouldQuery: true,
metaMessages: [buildGoalStartPrompt(goal)],
})
return null
} catch (error) {
onDone(error instanceof Error ? error.message : String(error), {
display: 'system',
})
return null
}
}