blob: 202b3a26380878f82483100cbeb559ecc5f3cc8d [file]
import { z } from 'zod';
import type {
PlanExecution,
PlanMutationResult,
PlanProposal,
PlanStepStatus,
PlanStore,
} from '@maka/core/plan';
import type { MakaTool } from './tool-runtime.js';
const MARKDOWN_PATTERN =
/(^|\n)\s{0,3}(?:#{1,6}\s|[-*+]\s|\d+[.)]\s|>\s|```|~~~)|!?(?:\[[^\]\n]+\]\([^)\n]+\))|(?:\*\*|__|`)/;
const plainTextSchema = (label: string) =>
z
.string()
.trim()
.min(1)
.refine((value) => !MARKDOWN_PATTERN.test(value), {
message: `${label} must be plain text without Markdown formatting`,
});
const stepDefinitionSchema = z.object({
id: z.string().trim().min(1),
title: plainTextSchema('Plan step title').max(30),
description: plainTextSchema('Plan step description'),
files: z.array(z.string().min(1)).optional(),
complexity: z.enum(['low', 'medium', 'high']).optional(),
});
const executionStepSchema = z.object({
id: z.string().min(1),
status: z.enum(['pending', 'in_progress', 'completed', 'skipped']),
note: z.string().min(1).optional(),
});
export type PlanToolResult =
| {
kind: 'plan_submitted';
proposal: PlanProposal;
storeVersion: number;
}
| {
kind: 'plan_progress_updated' | 'plan_execution_completed';
execution: PlanExecution;
storeVersion: number;
}
| {
kind: 'plan_execution_cancelled';
execution: PlanExecution;
storeVersion: number;
};
export function buildSubmitPlanTool(
planStore: PlanStore,
sourceExecutionId?: string,
): MakaTool<
{
title: string;
overview?: string;
steps: Array<{
id: string;
title: string;
description: string;
files?: string[];
complexity?: 'low' | 'medium' | 'high';
}>;
risks?: string[];
},
PlanToolResult
> {
return {
name: 'SubmitPlan',
description:
'Submit the finished implementation plan for user approval. Every step requires a concise plain-text title and a detailed plain-text description; do not use Markdown in either field. This ends the planning turn, so do not call it until the plan is ready to review.',
parameters: z.object({
title: z.string().min(1),
overview: z.string().min(1).optional(),
steps: z.array(stepDefinitionSchema).min(1).max(50),
risks: z.array(z.string().min(1)).max(20).optional(),
}),
permissionRequired: false,
impl: async (input, context) => {
const result = await planStore.submitProposal({
sessionId: context.sessionId,
turnId: context.turnId,
...(sourceExecutionId ? { sourceExecutionId } : {}),
...input,
});
return {
kind: 'plan_submitted',
proposal: result.state.proposals.find(
(proposal) => proposal.proposalId === result.state.latestProposalId,
)!,
storeVersion: result.state.storeVersion,
};
},
};
}
export function buildUpdatePlanTool(
planStore: PlanStore,
executionId: string,
): MakaTool<
{
steps: Array<{ id: string; status: PlanStepStatus; note?: string }>;
explanation?: string;
},
PlanToolResult
> {
return {
name: 'update_plan',
description:
'Update execution progress for the approved plan. Include every plan step and keep at most one step in_progress.',
parameters: z.object({
steps: z.array(executionStepSchema).min(1).max(50),
explanation: z.string().min(1).optional(),
}),
permissionRequired: false,
impl: async (input, context) => {
const result = await planStore.updateExecution({
sessionId: context.sessionId,
executionId,
...input,
});
return executionResult(result);
},
};
}
export function buildCancelPlanTool(
planStore: PlanStore,
executionId: string,
): MakaTool<{ reason: string }, PlanToolResult> {
return {
name: 'cancel_plan',
description:
'Cancel the active plan execution when the user explicitly asks to abandon it. Explain the user request in reason.',
parameters: z.object({ reason: z.string().min(1) }),
permissionRequired: false,
impl: async ({ reason }, context) => {
const result = await planStore.cancelExecution({
sessionId: context.sessionId,
executionId,
reason,
});
return executionResult(result);
},
};
}
function executionResult(result: PlanMutationResult): PlanToolResult {
const executionId =
'executionId' in result.event ? result.event.executionId : result.state.activeExecutionId;
const execution = result.state.executions.find(
(candidate) => candidate.executionId === executionId,
);
if (!execution) throw new Error('Plan execution projection is missing');
if (result.event.type === 'plan_execution_completed') {
return { kind: 'plan_execution_completed', execution, storeVersion: result.state.storeVersion };
}
if (result.event.type === 'plan_execution_cancelled') {
return { kind: 'plan_execution_cancelled', execution, storeVersion: result.state.storeVersion };
}
return { kind: 'plan_progress_updated', execution, storeVersion: result.state.storeVersion };
}