forked from EduCraft/curriculum-project-hub
2f79b7743f
Always expose Claude Agent SDK TodoWrite (todoFeatureEnabled) so multi-step runs can plan in the open. Parse TodoWrite payloads into a progress panel on the streaming Feishu card (completed/in_progress/pending) and filter raw TodoWrite noise out of the tool-use list.
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
/**
|
|
* Parse Claude Agent SDK TodoWrite tool input into a stable checklist model
|
|
* for Feishu card progress rendering (Manus-style live todos).
|
|
*/
|
|
|
|
export type AgentTodoStatus = "pending" | "in_progress" | "completed";
|
|
|
|
export interface AgentTodoItem {
|
|
readonly content: string;
|
|
readonly status: AgentTodoStatus;
|
|
/** Present-tense label while the item is active, when the model supplies it. */
|
|
readonly activeForm: string | undefined;
|
|
}
|
|
|
|
const STATUSES = new Set<AgentTodoStatus>(["pending", "in_progress", "completed"]);
|
|
|
|
/** True when the tool name is the SDK TodoWrite dispatcher. */
|
|
export function isTodoWriteTool(toolName: string): boolean {
|
|
return toolName === "TodoWrite" || toolName.endsWith("__TodoWrite");
|
|
}
|
|
|
|
/**
|
|
* Extract the full todo list from a TodoWrite tool_use input.
|
|
* Returns null when the payload is not a usable TodoWrite body.
|
|
*/
|
|
export function parseTodoWriteInput(input: unknown): readonly AgentTodoItem[] | null {
|
|
if (typeof input !== "object" || input === null || Array.isArray(input)) return null;
|
|
if (!("todos" in input)) return null;
|
|
const rawTodos = (input as { todos?: unknown }).todos;
|
|
if (!Array.isArray(rawTodos) || rawTodos.length === 0) return null;
|
|
|
|
const todos: AgentTodoItem[] = [];
|
|
for (const raw of rawTodos) {
|
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
const record = raw as Record<string, unknown>;
|
|
const content = typeof record.content === "string" ? record.content.trim() : "";
|
|
if (content === "") continue;
|
|
const statusRaw = typeof record.status === "string" ? record.status : "pending";
|
|
const status: AgentTodoStatus = STATUSES.has(statusRaw as AgentTodoStatus)
|
|
? (statusRaw as AgentTodoStatus)
|
|
: "pending";
|
|
const activeForm =
|
|
typeof record.activeForm === "string" && record.activeForm.trim() !== ""
|
|
? record.activeForm.trim()
|
|
: undefined;
|
|
todos.push({ content, status, activeForm });
|
|
}
|
|
return todos.length === 0 ? null : todos;
|
|
}
|
|
|
|
export function todoProgressSummary(todos: readonly AgentTodoItem[]): {
|
|
readonly completed: number;
|
|
readonly total: number;
|
|
readonly inProgress: number;
|
|
} {
|
|
let completed = 0;
|
|
let inProgress = 0;
|
|
for (const todo of todos) {
|
|
if (todo.status === "completed") completed += 1;
|
|
else if (todo.status === "in_progress") inProgress += 1;
|
|
}
|
|
return { completed, total: todos.length, inProgress };
|
|
}
|