[codex] integrate goal command workflow (#1025)

* feat: integrate goal command workflow

* fix: keep goal done visible

* fix: add goal done slash command

* fix: promote queued message on run start
This commit is contained in:
ekko
2026-05-25 19:26:23 +08:00
committed by GitHub
parent 0eab6a1125
commit badb17cf8e
30 changed files with 1535 additions and 85 deletions
@@ -15,6 +15,8 @@ type CommandName =
| 'abort'
| 'queue'
| 'plan'
| 'goal'
| 'subgoal'
| 'clear'
| 'title'
| 'compress'
@@ -34,6 +36,8 @@ interface SessionCommandContext {
bridge: AgentBridgeClient
profile: string
model?: string
provider?: string
model_groups?: Array<{ provider: string; models: string[] }>
instructions?: string
queueId?: string
runQueuedItem: (socket: Socket, sessionId: string, next: QueuedRun, fallbackProfile?: string) => void
@@ -45,6 +49,8 @@ const COMMAND_ALIASES: Record<string, CommandName> = {
abort: 'abort',
queue: 'queue',
plan: 'plan',
goal: 'goal',
subgoal: 'subgoal',
clear: 'clear',
title: 'title',
compress: 'compress',
@@ -120,24 +126,30 @@ export async function handleSessionCommand(
case 'status': {
const row = getSession(sessionId)
const bridgeStatus = await getBridgeSessionStatus(ctx, sessionId)
const bridgeRunning = bridgeStatus?.running === true
const isWorking = state.isWorking || bridgeRunning
const runId = state.runId || state.activeRunMarker || bridgeStatus?.currentRunId || null
emitCommand({
action: 'status',
terminal: !state.isWorking,
terminal: !isWorking,
message: [
`Status: ${state.isWorking ? 'running' : 'idle'}`,
`Status: ${isWorking ? 'running' : 'idle'}`,
`source: ${state.source || row?.source || 'cli'}`,
`profile: ${state.profile || ctx.profile || row?.profile || 'default'}`,
`model: ${ctx.model || row?.model || '-'}`,
`queue: ${state.queue.length}`,
`run: ${state.runId || state.activeRunMarker || '-'}`,
].join(', '),
isWorking: state.isWorking,
`run: ${runId || '-'}`,
bridgeStatus ? `bridge: ${bridgeRunning ? 'running' : 'idle'}` : null,
].filter(Boolean).join(', '),
isWorking,
isAborting: Boolean(state.isAborting),
queueLength: state.queue.length,
source: state.source || row?.source || 'cli',
profile: state.profile || ctx.profile || row?.profile || 'default',
model: ctx.model || row?.model || null,
runId: state.runId || state.activeRunMarker || null,
runId,
bridgeStatus,
})
return
}
@@ -161,6 +173,8 @@ export async function handleSessionCommand(
queue_id: queueId,
input: command.args,
model: ctx.model,
provider: ctx.provider,
model_groups: ctx.model_groups,
instructions: ctx.instructions,
profile: ctx.profile,
source: 'cli',
@@ -170,13 +184,7 @@ export async function handleSessionCommand(
event: 'run.queued',
session_id: sessionId,
queue_length: state.queue.length,
queued_messages: state.queue.map(item => ({
id: item.queue_id,
role: 'user',
content: contentBlocksToString(item.input),
timestamp: Math.floor(Date.now() / 1000),
queued: true,
})),
queued_messages: serializeVisibleQueuedMessages(state.queue),
})
emitCommand({
action: 'queue',
@@ -221,6 +229,8 @@ export async function handleSessionCommand(
displayRole: 'command',
storageMessage: displayCommand,
model: ctx.model,
provider: ctx.provider,
model_groups: ctx.model_groups,
instructions: ctx.instructions,
profile: ctx.profile,
source: 'cli',
@@ -233,15 +243,7 @@ export async function handleSessionCommand(
event: 'run.queued',
session_id: sessionId,
queue_length: state.queue.length,
queued_messages: state.queue.map(item => ({
id: item.queue_id,
role: typeof item.displayInput === 'string' && item.displayInput.trim().startsWith('/') ? 'command' : 'user',
content: item.displayInput === null
? (item.storageMessage || '')
: contentBlocksToString(item.displayInput ?? item.input),
timestamp: Math.floor(Date.now() / 1000),
queued: true,
})),
queued_messages: serializeVisibleQueuedMessages(state.queue),
})
return
}
@@ -255,6 +257,88 @@ export async function handleSessionCommand(
return
}
case 'goal':
case 'subgoal': {
const isGoalSet = command.name === 'goal'
&& Boolean(command.args)
&& !['status', 'pause', 'resume', 'clear', 'stop', 'done'].includes(command.args.toLowerCase())
if (state.isWorking && isGoalSet) {
emitCommand({
ok: false,
action: 'goal',
terminal: false,
message: 'Agent is running. Use /goal status, /goal pause, or /goal clear mid-run, or /abort before setting a new goal.',
})
return
}
const bridgeCommand = `${command.name}${command.args ? ` ${command.args}` : ''}`
let result
try {
result = await ctx.bridge.command(sessionId, bridgeCommand, ctx.profile)
} catch (err) {
emitCommand({
ok: false,
action: command.name,
terminal: !state.isWorking,
message: `Goal command failed: ${err instanceof Error ? err.message : String(err)}`,
})
return
}
if (result.clear_goal_continuations) {
const removed = removeGoalContinuationRuns(state)
if (removed > 0) emitQueuedState(ctx, sessionId, state)
}
const kickoffPrompt = typeof result.kickoff_prompt === 'string' ? result.kickoff_prompt.trim() : ''
const bridgeStatus = result.action === 'goal_status' || result.action === 'status'
? await getBridgeSessionStatus(ctx, sessionId)
: null
const message = formatGoalStatusMessage(String(result.message || ''), bridgeStatus)
const resultAction = String(result.action || command.name)
const action = (command.name === 'goal' || command.name === 'subgoal') && resultAction === 'clear'
? `${command.name}_clear`
: resultAction
emitCommand({
action,
terminal: !state.isWorking && !kickoffPrompt,
started: Boolean(kickoffPrompt),
message,
type: result.type || 'goal',
maxTurns: result.max_turns,
bridgeStatus,
})
if (!kickoffPrompt) return
const next: QueuedRun = {
queue_id: ctx.queueId || `queue_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
input: kickoffPrompt,
displayInput: null,
storageMessage: kickoffPrompt,
model: ctx.model,
provider: ctx.provider,
model_groups: ctx.model_groups,
instructions: ctx.instructions,
profile: ctx.profile,
source: 'cli',
originSocketId: ctx.socket.id,
}
if (state.isWorking) {
state.queue.push(next)
emitQueuedState(ctx, sessionId, state)
return
}
ctx.runQueuedItem(ctx.socket, sessionId, next, ctx.profile)
return
}
case 'clear': {
if (command.args === '--history') {
if (state.isWorking) {
@@ -462,6 +546,79 @@ function clearTransientRunState(state: SessionState) {
state.isAborting = false
}
function removeGoalContinuationRuns(state: SessionState): number {
const before = state.queue.length
state.queue = state.queue.filter(item => !item.goalContinuation)
return before - state.queue.length
}
function emitQueuedState(ctx: SessionCommandContext, sessionId: string, state: SessionState) {
emitToSession(ctx.nsp, ctx.socket, sessionId, 'run.queued', {
event: 'run.queued',
session_id: sessionId,
queue_length: state.queue.length,
queued_messages: serializeVisibleQueuedMessages(state.queue),
})
}
function serializeVisibleQueuedMessages(queue: QueuedRun[]) {
return queue.filter(item => item.displayInput !== null).map(item => ({
id: item.queue_id,
role: item.displayRole || (typeof item.displayInput === 'string' && item.displayInput.trim().startsWith('/') ? 'command' : 'user'),
content: contentBlocksToString(item.displayInput ?? item.input),
timestamp: Math.floor(Date.now() / 1000),
queued: true,
}))
}
type BridgeSessionStatus = {
exists: boolean
running: boolean
currentRunId: string | null
messageCount: number
}
async function getBridgeSessionStatus(ctx: SessionCommandContext, sessionId: string): Promise<BridgeSessionStatus | null> {
try {
const raw = await ctx.bridge.status(sessionId, ctx.profile) as Record<string, unknown>
return {
exists: raw.exists === true,
running: raw.running === true,
currentRunId: typeof raw.current_run_id === 'string' && raw.current_run_id.trim()
? raw.current_run_id
: null,
messageCount: typeof raw.message_count === 'number' && Number.isFinite(raw.message_count)
? raw.message_count
: 0,
}
} catch (err) {
logger.debug({ err, sessionId }, '[chat-run-socket] bridge status lookup failed')
return null
}
}
function formatGoalStatusMessage(message: string, bridgeStatus: BridgeSessionStatus | null): string {
if (!bridgeStatus) return message
const lines = [message]
if (bridgeStatus.running) {
const progress = parseGoalTurnProgress(message)
lines.push(progress
? `Current turn: ${Math.min(progress.used + 1, progress.max)}/${progress.max} running (completed turns: ${progress.used}/${progress.max}; count updates after the judge).`
: 'Current turn: running (turn count updates after the judge).')
}
lines.push(`Run: ${bridgeStatus.running ? 'running' : 'idle'}${bridgeStatus.currentRunId ? ` (${bridgeStatus.currentRunId})` : ''}`)
return lines.filter(Boolean).join('\n')
}
function parseGoalTurnProgress(message: string): { used: number; max: number } | null {
const match = message.match(/\b(\d+)\s*\/\s*(\d+)\s+turns\b/i)
if (!match) return null
const used = Number(match[1])
const max = Number(match[2])
if (!Number.isFinite(used) || !Number.isFinite(max) || max <= 0) return null
return { used, max }
}
function ensureCommandSession(sessionId: string, ctx: SessionCommandContext) {
if (getSession(sessionId)) return
createSession({