| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import type { |
| HistoryMessage, |
| InteractionMessage, |
| ToolCallMessage, |
| } from '@moonshot-ai/kap-server/protocol'; |
|
|
| export interface PlanReview { |
| readonly state: 'pending' | 'approved' | 'rejected' | 'cancelled'; |
| readonly selectedOption?: string; |
| readonly feedback?: string; |
| } |
|
|
| export interface PlanInfo { |
| readonly toolCallId: string; |
| readonly turnId: string; |
| |
| readonly source: 'interaction' | 'display' | 'output'; |
| readonly plan: string; |
| readonly path?: string; |
| readonly options?: readonly { label: string; description?: string }[]; |
| readonly review?: PlanReview; |
| } |
|
|
| export function projectPlans( |
| messages: readonly HistoryMessage[], |
| toolCallId?: string, |
| ): PlanInfo[] { |
| const interactions: InteractionMessage[] = []; |
| const revisionPaths: string[] = []; |
| for (const message of messages) { |
| if (message.type === 'interaction') interactions.push(message); |
| if (message.type === 'system' && message.subtype === 'plan.revision') { |
| const path = readRevisionPath(message.payload); |
| if (path !== undefined) revisionPaths.push(path); |
| } |
| } |
| const plans: PlanInfo[] = []; |
| for (const message of messages) { |
| if (message.type !== 'tool_call' || message.name !== 'ExitPlanMode') continue; |
| if (toolCallId !== undefined && message.tool_call_id !== toolCallId) continue; |
| const info = projectPlanCall(message, interactions); |
| if (info === undefined) continue; |
| plans.push( |
| info.path === undefined && revisionPaths.length > 0 |
| ? { ...info, path: revisionPaths.at(-1) } |
| : info, |
| ); |
| } |
| return plans; |
| } |
|
|
| function projectPlanCall( |
| call: ToolCallMessage, |
| interactions: readonly InteractionMessage[], |
| ): PlanInfo | undefined { |
| const interaction = interactions.find( |
| (candidate) => |
| candidate.kind === 'approval' && |
| (candidate.interaction_id === call.approval_id || |
| (call.approval_id === undefined && candidate.tool_call_id === call.tool_call_id)), |
| ); |
| const review = readPlanReview(interaction); |
| if (interaction !== undefined && interaction.kind === 'approval') { |
| const fromInteraction = readPlanReviewDisplay(interaction.request?.tool_input_display); |
| if (fromInteraction !== undefined) { |
| return { |
| toolCallId: call.tool_call_id, |
| turnId: call.turn_id, |
| source: 'interaction', |
| ...fromInteraction, |
| review, |
| }; |
| } |
| } |
| const fromDisplay = readPlanReviewDisplay(call.display); |
| if (fromDisplay !== undefined) { |
| return { |
| toolCallId: call.tool_call_id, |
| turnId: call.turn_id, |
| source: 'display', |
| ...fromDisplay, |
| review, |
| }; |
| } |
| const fromOutput = parsePlanFromOutput(call.output); |
| if (fromOutput !== undefined) { |
| return { |
| toolCallId: call.tool_call_id, |
| turnId: call.turn_id, |
| source: 'output', |
| ...fromOutput, |
| review, |
| }; |
| } |
| return undefined; |
| } |
|
|
| function readPlanReview(interaction: InteractionMessage | undefined): PlanReview | undefined { |
| if (interaction === undefined || interaction.kind !== 'approval') return undefined; |
| const state = interaction.status; |
| if (state !== 'pending' && state !== 'approved' && state !== 'rejected' && state !== 'cancelled') { |
| return undefined; |
| } |
| const response = interaction.response; |
| const selected = |
| typeof response?.selected_label === 'string' && response.selected_label.length > 0 |
| ? response.selected_label |
| : undefined; |
| const feedback = |
| typeof response?.feedback === 'string' && response.feedback.length > 0 |
| ? response.feedback |
| : undefined; |
| return { state, selectedOption: selected, feedback }; |
| } |
|
|
| interface PlanReviewDisplayInfo { |
| readonly plan: string; |
| readonly path?: string; |
| readonly options?: readonly { label: string; description?: string }[]; |
| } |
|
|
| function readPlanReviewDisplay(display: unknown): PlanReviewDisplayInfo | undefined { |
| if (display === null || typeof display !== 'object') return undefined; |
| const d = display as { kind?: unknown; plan?: unknown; path?: unknown; options?: unknown }; |
| if (d.kind !== 'plan_review' || typeof d.plan !== 'string' || d.plan.trim().length === 0) { |
| return undefined; |
| } |
| const options = Array.isArray(d.options) |
| ? d.options |
| .map((option: unknown): { label: string; description?: string } | null => { |
| if (option === null || typeof option !== 'object') return null; |
| const o = option as { label?: unknown; description?: unknown }; |
| if (typeof o.label !== 'string' || o.label.length === 0) return null; |
| return { |
| label: o.label, |
| description: typeof o.description === 'string' ? o.description : undefined, |
| }; |
| }) |
| .filter((o): o is { label: string; description?: string } => o !== null) |
| : undefined; |
| return { |
| plan: d.plan, |
| path: typeof d.path === 'string' ? d.path : undefined, |
| options: options !== undefined && options.length > 0 ? options : undefined, |
| }; |
| } |
|
|
| function readRevisionPath(payload: unknown): string | undefined { |
| if (payload === null || typeof payload !== 'object') return undefined; |
| const path = (payload as { path?: unknown }).path; |
| return typeof path === 'string' && path.length > 0 ? path : undefined; |
| } |
|
|
| const PLAN_SAVED_TO_MARKER = 'Plan saved to: '; |
| const PLAN_BODY_MARKERS = ['## Approved Plan:\n', '## Plan (auto-approved, not user-reviewed):\n']; |
|
|
| function parsePlanFromOutput(output: unknown): { plan: string; path?: string } | undefined { |
| if (typeof output !== 'string') return undefined; |
| let path: string | undefined; |
| for (const line of output.split('\n')) { |
| if (line.startsWith(PLAN_SAVED_TO_MARKER)) { |
| path = line.slice(PLAN_SAVED_TO_MARKER.length).trim() || undefined; |
| break; |
| } |
| } |
| for (const marker of PLAN_BODY_MARKERS) { |
| const index = output.indexOf(marker); |
| if (index === -1) continue; |
| const plan = output.slice(index + marker.length); |
| if (plan.trim().length > 0) return { plan, path }; |
| } |
| return undefined; |
| } |
|
|