import { TaskStatus, isTerminalStatus } from '../task/task-state.js'; import type { TaskState, PendingQuestion } from '../task/task-state.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function taskDir(task: TaskState): string { return `~/.mcp-codex-worker/tasks/${task.id}`; } function progressHint(task: TaskState): string { return `Quick progress: \`tail -1 ${taskDir(task)}/timeline.log\` — shows the last meaningful event.`; } function logHint(task: TaskState): string { return `Read \`task:///${task.id}/log\` for summary, or \`task:///${task.id}/events\` for the full event trace.`; } // --------------------------------------------------------------------------- // Per-tool guidance builders // --------------------------------------------------------------------------- /** * Guidance appended to spawn-task responses. * Goal: tell the orchestrator to launch more tasks, then wait, use wc -l. */ export function buildSpawnGuidance(task: TaskState): string[] { const lines: string[] = ['', '---', '**What to do next:**']; switch (task.status) { case TaskStatus.PENDING: case TaskStatus.RUNNING: case TaskStatus.RATE_LIMITED: lines.push( '- If you still have more agents to launch, launch them now — all agents run in parallel.', `- Call \`wait-task\` with \`task_id: "${task.id}"\` to block until the agent finishes or needs input.`, `- ${progressHint(task)}`, `- Live monitoring: \`tail -f ${taskDir(task)}/timeline.log\` — streams progress in real time.`, '- Read `task:///all` for a scoreboard of all tasks.', `- \`input_required\` → agent needs input — answer via \`respond-task\`.`, ); break; case TaskStatus.WAITING_ANSWER: lines.push('- **ACTION REQUIRED** — the agent paused immediately and is waiting for your input.'); if (task.pendingQuestions.length > 0) { lines.push(...formatPendingQuestionGuidance(task.id, task.pendingQuestions[0]!)); } lines.push(`- After responding, call \`wait-task\` with \`task_id: "${task.id}"\` to resume monitoring.`); break; case TaskStatus.COMPLETED: lines.push( '- The agent finished successfully.', `- ${logHint(task)}`, `- Read \`task:///${task.id}\` for the full result detail.`, `- Use \`message-task\` with \`task_id: "${task.id}"\` to send follow-up instructions on the same session.`, ); break; case TaskStatus.FAILED: case TaskStatus.TIMED_OUT: lines.push( `- The agent failed: ${task.error ?? 'unknown error'}`, `- Read \`task:///${task.id}/events\` for the raw event trace to diagnose the failure.`, `- ${logHint(task)}`, '- To retry: spawn a new task with the same prompt. The failed task\'s logs are preserved on disk.', ); if (task.error?.includes('AUTH_TOKEN_EXPIRED')) { lines.push('- **Auth fix required:** run `codex auth login` to refresh your token, then retry.'); } break; default: break; } return lines; } /** * Guidance appended to wait-task responses. * Three branches: terminal, waiting_answer, still working. */ export function buildWaitGuidance(task: TaskState): string[] { const lines: string[] = ['', '---']; if (isTerminalStatus(task.status)) { // Terminal state — task is done const statusLabel = task.status === TaskStatus.COMPLETED ? 'completed successfully' : task.status === TaskStatus.CANCELLED ? 'was cancelled' : `failed: ${task.error ?? 'unknown error'}`; lines.push(`**Task ${statusLabel}.**`); lines.push( `- The output is included above in the \`output\` field.`, `- Read \`task:///${task.id}\` for the full result detail with metadata.`, `- Read \`task:///${task.id}/timeline\` for the execution timeline.`, ); if (task.status === TaskStatus.COMPLETED) { lines.push(`- To continue this work, spawn a new task in the same \`cwd\`.`); } else { lines.push(`- Read \`task:///${task.id}/events\` for the raw event trace to diagnose the failure.`); lines.push('- To retry: spawn a new task with the same prompt.'); } if (task.error?.includes('AUTH_TOKEN_EXPIRED')) { lines.push('- **Auth fix required:** run `codex auth login` to refresh your token, then retry.'); } return lines; } if (task.status === TaskStatus.WAITING_ANSWER) { lines.push('**ACTION REQUIRED** — the agent is paused and waiting for your input.'); if (task.pendingQuestions.length > 0) { lines.push(...formatPendingQuestionGuidance(task.id, task.pendingQuestions[0]!)); } lines.push(`- After responding, call \`wait-task\` with \`task_id: "${task.id}"\` to resume monitoring.`); return lines; } // Still working — wait timed out lines.push('**Task is still working** (wait timed out — the agent is not stuck).'); lines.push( `- Call \`wait-task\` again with \`task_id: "${task.id}"\` and a longer \`timeout_ms\` (e.g. 120000).`, `- ${progressHint(task)}`, `- Read \`task:///${task.id}/timeline\` for a formatted progress summary.`, '- If you have other tasks to check on, do that now and come back.', '- Read `task:///all` for the full scoreboard.', ); return lines; } /** * Guidance appended to respond-task responses. * Two branches: task resumed (working), task already terminal. */ export function buildRespondGuidance(task: TaskState): string[] { const lines: string[] = ['', '---']; if (isTerminalStatus(task.status)) { lines.push(`**Task is no longer running** (status: ${task.status}).`); lines.push( '- The task reached a terminal state before your response could be delivered.', `- Read \`task:///${task.id}\` for the final state.`, `- Read \`task:///${task.id}/events\` for the event trace.`, '- To retry: spawn a new task with the same prompt.', ); return lines; } lines.push('**Answer submitted — the agent is resuming work.**'); lines.push( `- Call \`wait-task\` with \`task_id: "${task.id}"\` to block until the agent finishes or needs more input.`, `- ${progressHint(task)}`, ); return lines; } /** * Guidance appended to message-task responses. * The task just received a follow-up turn and is back to RUNNING. */ export function buildMessageGuidance(task: TaskState): string[] { return [ '', '---', '**Follow-up message sent — the agent is resuming work.**', `- Call \`wait-task\` with \`task_id: "${task.id}"\` to block until the agent finishes or needs input.`, `- ${progressHint(task)}`, '- Read `task:///all` for a scoreboard of all tasks.', ]; } /** * Guidance for message-task when the task is already in a terminal state. * Returns structured advice instead of a thrown error. */ export function buildMessageTerminalGuidance(task: TaskState): string[] { const lines: string[] = ['', '---']; lines.push(`**Cannot send follow-up — task is ${task.status}.**`); if (task.status === TaskStatus.COMPLETED) { lines.push( '- The task already finished successfully. To continue this work, spawn a new task with the same `cwd`.', `- Read \`task:///${task.id}\` for the final result.`, `- Read \`task:///${task.id}/events\` for the event trace.`, ); } else { lines.push( `- The task ${task.status === TaskStatus.CANCELLED ? 'was cancelled' : 'failed'}: ${task.error ?? 'unknown error'}`, `- Read \`task:///${task.id}/events\` for the event trace.`, '- To retry: spawn a new task with the same prompt.', ); } return lines; } /** * Guidance appended to cancel-task responses. */ export function buildCancelGuidance(summary: { cancelled: string[]; alreadyTerminal: string[]; notFound: string[]; }): string[] { const lines: string[] = ['', '---', '**What to do next:**']; if (summary.cancelled.length > 0) { const ids = summary.cancelled.map(id => `\`${id}\``).join(', '); lines.push(`- Cancelled ${summary.cancelled.length} task(s): ${ids}. Read \`task:////events\` for partial output.`); } if (summary.alreadyTerminal.length > 0) { lines.push(`- ${summary.alreadyTerminal.length} task(s) were already in a terminal state.`); } if (summary.notFound.length > 0) { lines.push( `- ${summary.notFound.length} task ID(s) not found: ${summary.notFound.join(', ')}`, ' (Tasks are removed from memory ~5 min after completion. Read `task:///` which falls back to disk.)', ); } lines.push( '- Read `task:///all` for the updated scoreboard.', '- To resume cancelled work, spawn new tasks with the same prompts.', ); return lines; } // --------------------------------------------------------------------------- // Pending question formatting (shared helper) // --------------------------------------------------------------------------- function formatPendingQuestionGuidance(taskId: string, pq: PendingQuestion): string[] { const lines: string[] = []; switch (pq.type) { case 'command_approval': lines.push( `- The agent wants to run: \`${pq.command}\``, `- To approve: \`respond-task\` with \`{"task_id":"${taskId}","type":"command_approval","decision":"accept"}\``, `- To reject: \`respond-task\` with \`{"task_id":"${taskId}","type":"command_approval","decision":"reject"}\``, ); break; case 'file_approval': lines.push( `- The agent wants to edit ${pq.fileChanges.length} file(s): ${pq.fileChanges.map(f => f.path).join(', ')}`, `- To approve: \`respond-task\` with \`{"task_id":"${taskId}","type":"file_approval","decision":"accept"}\``, `- To reject: \`respond-task\` with \`{"task_id":"${taskId}","type":"file_approval","decision":"reject"}\``, ); break; case 'user_input': lines.push('- The agent is asking:'); for (const q of pq.questions) { lines.push(` - **${q.text}** (id: \`${q.id}\`)`); if (q.options && q.options.length > 0) { for (let i = 0; i < q.options.length; i++) { lines.push(` ${i + 1}. ${q.options[i]}`); } } } { const exampleAnswers: Record = {}; for (const q of pq.questions) { exampleAnswers[q.id] = q.options?.length ? '1' : 'your answer'; } lines.push( `- To answer: \`respond-task\` with \`${JSON.stringify({ task_id: taskId, type: 'user_input', answers: exampleAnswers })}\``, ); } break; case 'elicitation': lines.push( `- MCP server "${pq.serverName ?? 'unknown'}" is requesting input: ${pq.message}`, `- To accept: \`respond-task\` with \`{"task_id":"${taskId}","type":"elicitation","action":"accept"}\``, `- To decline: \`respond-task\` with \`{"task_id":"${taskId}","type":"elicitation","action":"decline"}\``, ); break; case 'dynamic_tool': lines.push( `- Dynamic tool \`${pq.toolName}\` needs a response.`, `- To respond: \`respond-task\` with \`{"task_id":"${taskId}","type":"dynamic_tool","result":"your result"}\``, ); break; } return lines; }