forked from EduCraft/curriculum-project-hub
Merge pull request 'fix(hub): render Feishu checklist from TaskCreate/TaskUpdate' (#28) from fix/task-checklist-card-panel into main
This commit is contained in:
+11
-3
@@ -47,7 +47,7 @@ export type StreamEvent =
|
|||||||
| { readonly type: "thinking-delta"; readonly text: string }
|
| { readonly type: "thinking-delta"; readonly text: string }
|
||||||
| { readonly type: "tool-start"; readonly toolName: string; readonly toolUseId: string }
|
| { readonly type: "tool-start"; readonly toolName: string; readonly toolUseId: string }
|
||||||
| { readonly type: "tool-end"; readonly toolName: string; readonly toolUseId: string; readonly input: unknown; readonly durationMs?: number }
|
| { readonly type: "tool-end"; readonly toolName: string; readonly toolUseId: string; readonly input: unknown; readonly durationMs?: number }
|
||||||
| { readonly type: "tool-result"; readonly toolUseId: string; readonly toolName: string; readonly result: string; readonly isError: boolean; readonly durationMs?: number }
|
| { readonly type: "tool-result"; readonly toolUseId: string; readonly toolName: string; readonly result: string; readonly isError: boolean; readonly input?: unknown; readonly durationMs?: number }
|
||||||
| { readonly type: "finish" };
|
| { readonly type: "finish" };
|
||||||
|
|
||||||
export type StreamCallback = (event: StreamEvent) => void;
|
export type StreamCallback = (event: StreamEvent) => void;
|
||||||
@@ -232,8 +232,9 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
|
|||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track tool start timestamps for duration calculation
|
// Track tool start timestamps and names/inputs for duration + tool-result attribution.
|
||||||
const toolStartTimestamps = new Map<string, number>();
|
const toolStartTimestamps = new Map<string, number>();
|
||||||
|
const toolMetaByUseId = new Map<string, { readonly name: string; readonly input: unknown }>();
|
||||||
|
|
||||||
for await (const message of conversation) {
|
for await (const message of conversation) {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
@@ -254,6 +255,10 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
|
|||||||
if (evt.type === "content_block_start" && evt.content_block.type === "tool_use") {
|
if (evt.type === "content_block_start" && evt.content_block.type === "tool_use") {
|
||||||
const toolUseId = evt.content_block.id;
|
const toolUseId = evt.content_block.id;
|
||||||
toolStartTimestamps.set(toolUseId, Date.now());
|
toolStartTimestamps.set(toolUseId, Date.now());
|
||||||
|
toolMetaByUseId.set(toolUseId, {
|
||||||
|
name: evt.content_block.name,
|
||||||
|
input: undefined,
|
||||||
|
});
|
||||||
onStream?.({ type: "tool-start", toolName: evt.content_block.name, toolUseId });
|
onStream?.({ type: "tool-start", toolName: evt.content_block.name, toolUseId });
|
||||||
}
|
}
|
||||||
if (evt.type === "content_block_stop") {
|
if (evt.type === "content_block_stop") {
|
||||||
@@ -275,6 +280,7 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
|
|||||||
const durationMs = toolStartTimestamps.has(block.id)
|
const durationMs = toolStartTimestamps.has(block.id)
|
||||||
? Date.now() - (toolStartTimestamps.get(block.id) ?? 0)
|
? Date.now() - (toolStartTimestamps.get(block.id) ?? 0)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
toolMetaByUseId.set(block.id, { name: block.name, input: block.input });
|
||||||
onStream?.({
|
onStream?.({
|
||||||
type: "tool-end",
|
type: "tool-end",
|
||||||
toolName: block.name,
|
toolName: block.name,
|
||||||
@@ -308,12 +314,14 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
|
|||||||
const isError = block.is_error === true;
|
const isError = block.is_error === true;
|
||||||
const resultText = extractToolResultText(block.content);
|
const resultText = extractToolResultText(block.content);
|
||||||
const durationMs = toolStartTimestamps.get(toolUseId);
|
const durationMs = toolStartTimestamps.get(toolUseId);
|
||||||
|
const meta = toolMetaByUseId.get(toolUseId);
|
||||||
onStream?.({
|
onStream?.({
|
||||||
type: "tool-result",
|
type: "tool-result",
|
||||||
toolUseId,
|
toolUseId,
|
||||||
toolName: toolUseId,
|
toolName: meta?.name ?? toolUseId,
|
||||||
result: resultText,
|
result: resultText,
|
||||||
isError,
|
isError,
|
||||||
|
...(meta?.input !== undefined ? { input: meta.input } : {}),
|
||||||
...(durationMs !== undefined ? { durationMs: Date.now() - durationMs } : {}),
|
...(durationMs !== undefined ? { durationMs: Date.now() - durationMs } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-5
@@ -1,11 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Parse Claude Agent SDK TodoWrite tool input into a stable checklist model
|
* Parse agent checklist tools into a stable model for Feishu card progress.
|
||||||
* for Feishu card progress rendering (Manus-style live todos).
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Claude TodoWrite (and hub mcp todo_write): full-list replace
|
||||||
|
* - Claude TaskCreate / TaskUpdate tools used in headless agent mode
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type AgentTodoStatus = "pending" | "in_progress" | "completed";
|
export type AgentTodoStatus = "pending" | "in_progress" | "completed";
|
||||||
|
|
||||||
export interface AgentTodoItem {
|
export interface AgentTodoItem {
|
||||||
|
/** Present for Task* tools; optional for whole-list TodoWrite payloads. */
|
||||||
|
readonly id: string | undefined;
|
||||||
readonly content: string;
|
readonly content: string;
|
||||||
readonly status: AgentTodoStatus;
|
readonly status: AgentTodoStatus;
|
||||||
/** Present-tense label while the item is active, when the model supplies it. */
|
/** Present-tense label while the item is active, when the model supplies it. */
|
||||||
@@ -25,9 +30,25 @@ export function isTodoWriteTool(toolName: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTaskChecklistTool(toolName: string): boolean {
|
||||||
|
const base = stripToolSuffix(toolName);
|
||||||
|
return (
|
||||||
|
base === "TaskCreate" ||
|
||||||
|
base === "TaskUpdate" ||
|
||||||
|
base === "TaskList" ||
|
||||||
|
base === "TaskGet" ||
|
||||||
|
base === "TaskStop" ||
|
||||||
|
base === "TaskOutput"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChecklistProgressTool(toolName: string): boolean {
|
||||||
|
return isTodoWriteTool(toolName) || isTaskChecklistTool(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the full todo list from a TodoWrite tool_use input.
|
* Extract the full todo list from a TodoWrite / todo_write tool_use input.
|
||||||
* Returns null when the payload is not a usable TodoWrite body.
|
* Returns null when the payload is not a usable body.
|
||||||
*/
|
*/
|
||||||
export function parseTodoWriteInput(input: unknown): readonly AgentTodoItem[] | null {
|
export function parseTodoWriteInput(input: unknown): readonly AgentTodoItem[] | null {
|
||||||
if (typeof input !== "object" || input === null || Array.isArray(input)) return null;
|
if (typeof input !== "object" || input === null || Array.isArray(input)) return null;
|
||||||
@@ -49,11 +70,40 @@ export function parseTodoWriteInput(input: unknown): readonly AgentTodoItem[] |
|
|||||||
typeof record.activeForm === "string" && record.activeForm.trim() !== ""
|
typeof record.activeForm === "string" && record.activeForm.trim() !== ""
|
||||||
? record.activeForm.trim()
|
? record.activeForm.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
todos.push({ content, status, activeForm });
|
const id =
|
||||||
|
typeof record.id === "string" && record.id.trim() !== "" ? record.id.trim() : undefined;
|
||||||
|
todos.push({ id, content, status, activeForm });
|
||||||
}
|
}
|
||||||
return todos.length === 0 ? null : todos;
|
return todos.length === 0 ? null : todos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fold a Task* / TodoWrite tool event into the running checklist.
|
||||||
|
* Returns null when the event does not change checklist state.
|
||||||
|
*/
|
||||||
|
export function applyChecklistToolEvent(
|
||||||
|
current: readonly AgentTodoItem[],
|
||||||
|
params: {
|
||||||
|
readonly toolName: string;
|
||||||
|
readonly input: unknown;
|
||||||
|
readonly result: unknown;
|
||||||
|
readonly toolUseId?: string | undefined;
|
||||||
|
},
|
||||||
|
): readonly AgentTodoItem[] | null {
|
||||||
|
if (isTodoWriteTool(params.toolName)) {
|
||||||
|
return parseTodoWriteInput(params.input);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseName = stripToolSuffix(params.toolName);
|
||||||
|
if (baseName === "TaskCreate") {
|
||||||
|
return applyTaskCreate(current, params.input, params.result, params.toolUseId);
|
||||||
|
}
|
||||||
|
if (baseName === "TaskUpdate") {
|
||||||
|
return applyTaskUpdate(current, params.input);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function todoProgressSummary(todos: readonly AgentTodoItem[]): {
|
export function todoProgressSummary(todos: readonly AgentTodoItem[]): {
|
||||||
readonly completed: number;
|
readonly completed: number;
|
||||||
readonly total: number;
|
readonly total: number;
|
||||||
@@ -67,3 +117,150 @@ export function todoProgressSummary(todos: readonly AgentTodoItem[]): {
|
|||||||
}
|
}
|
||||||
return { completed, total: todos.length, inProgress };
|
return { completed, total: todos.length, inProgress };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripToolSuffix(toolName: string): string {
|
||||||
|
// SDK sometimes emits TaskCreate_0 sequential copies in the card title path.
|
||||||
|
const bare = toolName.includes("__") ? (toolName.split("__").pop() ?? toolName) : toolName;
|
||||||
|
return bare.replace(/_\d+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTaskCreate(
|
||||||
|
current: readonly AgentTodoItem[],
|
||||||
|
input: unknown,
|
||||||
|
result: unknown,
|
||||||
|
toolUseId: string | undefined,
|
||||||
|
): readonly AgentTodoItem[] | null {
|
||||||
|
if (typeof input !== "object" || input === null || Array.isArray(input)) return null;
|
||||||
|
const record = input as Record<string, unknown>;
|
||||||
|
const subject =
|
||||||
|
typeof record.subject === "string"
|
||||||
|
? record.subject.trim()
|
||||||
|
: typeof record.description === "string"
|
||||||
|
? record.description.trim()
|
||||||
|
: "";
|
||||||
|
if (subject === "") return null;
|
||||||
|
const activeForm =
|
||||||
|
typeof record.activeForm === "string" && record.activeForm.trim() !== ""
|
||||||
|
? record.activeForm.trim()
|
||||||
|
: undefined;
|
||||||
|
const idFromResult = extractTaskIdFromResult(result);
|
||||||
|
const provisionalId = toolUseId ?? `task-${current.length + 1}`;
|
||||||
|
const id = idFromResult ?? provisionalId;
|
||||||
|
|
||||||
|
// Replace any provisional row for this tool use or same pending subject.
|
||||||
|
const without = current.filter(
|
||||||
|
(t) =>
|
||||||
|
t.id !== provisionalId &&
|
||||||
|
t.id !== toolUseId &&
|
||||||
|
!(t.content === subject && t.status === "pending" && t.id !== id),
|
||||||
|
);
|
||||||
|
const existing = without.find((t) => taskIdsMatch(t.id, id));
|
||||||
|
if (existing !== undefined) {
|
||||||
|
return without.map((t) =>
|
||||||
|
taskIdsMatch(t.id, id)
|
||||||
|
? {
|
||||||
|
id,
|
||||||
|
content: subject,
|
||||||
|
status: existing.status,
|
||||||
|
activeForm: activeForm ?? existing.activeForm,
|
||||||
|
}
|
||||||
|
: t,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...without,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
content: subject,
|
||||||
|
status: "pending",
|
||||||
|
activeForm,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTaskUpdate(
|
||||||
|
current: readonly AgentTodoItem[],
|
||||||
|
input: unknown,
|
||||||
|
): readonly AgentTodoItem[] | null {
|
||||||
|
if (typeof input !== "object" || input === null || Array.isArray(input)) return null;
|
||||||
|
const record = input as Record<string, unknown>;
|
||||||
|
const taskId =
|
||||||
|
typeof record.taskId === "string"
|
||||||
|
? record.taskId.trim()
|
||||||
|
: typeof record.id === "string"
|
||||||
|
? record.id.trim()
|
||||||
|
: "";
|
||||||
|
if (taskId === "") return null;
|
||||||
|
|
||||||
|
const statusRaw = typeof record.status === "string" ? record.status : undefined;
|
||||||
|
if (statusRaw === "deleted") {
|
||||||
|
const next = current.filter((t) => !taskIdsMatch(t.id, taskId));
|
||||||
|
return next.length === current.length ? null : next;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status: AgentTodoStatus | undefined =
|
||||||
|
statusRaw !== undefined && STATUSES.has(statusRaw as AgentTodoStatus)
|
||||||
|
? (statusRaw as AgentTodoStatus)
|
||||||
|
: undefined;
|
||||||
|
const subject =
|
||||||
|
typeof record.subject === "string" && record.subject.trim() !== ""
|
||||||
|
? record.subject.trim()
|
||||||
|
: undefined;
|
||||||
|
const activeForm =
|
||||||
|
typeof record.activeForm === "string" && record.activeForm.trim() !== ""
|
||||||
|
? record.activeForm.trim()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (status === undefined && subject === undefined && activeForm === undefined) return null;
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
const next = current.map((todo) => {
|
||||||
|
if (!taskIdsMatch(todo.id, taskId)) return todo;
|
||||||
|
found = true;
|
||||||
|
return {
|
||||||
|
id: todo.id ?? taskId,
|
||||||
|
content: subject ?? todo.content,
|
||||||
|
status: status ?? todo.status,
|
||||||
|
activeForm: activeForm ?? todo.activeForm,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (found) return next;
|
||||||
|
|
||||||
|
// Update arrived before create (or id mismatch): synthesize a row so the
|
||||||
|
// teacher still sees lifecycle updates.
|
||||||
|
if (subject === undefined && status === undefined) return null;
|
||||||
|
return [
|
||||||
|
...current,
|
||||||
|
{
|
||||||
|
id: taskId,
|
||||||
|
content: subject ?? `任务 ${taskId}`,
|
||||||
|
status: status ?? "pending",
|
||||||
|
activeForm,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTaskIdFromResult(result: unknown): string | undefined {
|
||||||
|
const text =
|
||||||
|
typeof result === "string"
|
||||||
|
? result
|
||||||
|
: result !== null && typeof result === "object" && "content" in result
|
||||||
|
? String((result as { content: unknown }).content)
|
||||||
|
: "";
|
||||||
|
if (text === "") return undefined;
|
||||||
|
const hash = text.match(/Task\s*#\s*([0-9A-Za-z_-]+)/i);
|
||||||
|
if (hash?.[1]) return hash[1];
|
||||||
|
const bare = text.match(/\bid\s*[:=]\s*["']?([0-9A-Za-z_-]+)/i);
|
||||||
|
if (bare?.[1]) return bare[1];
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskIdsMatch(a: string | undefined, b: string): boolean {
|
||||||
|
if (a === undefined) return false;
|
||||||
|
return normalizeTaskId(a) === normalizeTaskId(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTaskId(id: string | undefined): string {
|
||||||
|
if (id === undefined) return "";
|
||||||
|
return id.trim().replace(/^#/, "");
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,11 +59,17 @@ function toolIcon(toolName: string): string {
|
|||||||
|
|
||||||
function isTodoToolName(toolName: string): boolean {
|
function isTodoToolName(toolName: string): boolean {
|
||||||
const lower = toolName.toLowerCase();
|
const lower = toolName.toLowerCase();
|
||||||
|
const bare = lower.includes("__") ? (lower.split("__").pop() ?? lower) : lower;
|
||||||
|
const stripped = bare.replace(/_\d+$/, "");
|
||||||
return (
|
return (
|
||||||
lower === "todowrite" ||
|
stripped === "todowrite" ||
|
||||||
lower === "todo_write" ||
|
stripped === "todo_write" ||
|
||||||
lower.endsWith("__todo_write") ||
|
stripped === "taskcreate" ||
|
||||||
lower.endsWith("__todowrite")
|
stripped === "taskupdate" ||
|
||||||
|
stripped === "tasklist" ||
|
||||||
|
stripped === "taskget" ||
|
||||||
|
stripped === "taskstop" ||
|
||||||
|
stripped === "taskoutput"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ import {
|
|||||||
getToolUseTraceSteps,
|
getToolUseTraceSteps,
|
||||||
} from "./trace-store.js";
|
} from "./trace-store.js";
|
||||||
import { buildAgentCard, type CardPhase } from "./builder.js";
|
import { buildAgentCard, type CardPhase } from "./builder.js";
|
||||||
import { isTodoWriteTool, parseTodoWriteInput, type AgentTodoItem } from "../../agent/todoList.js";
|
import {
|
||||||
|
applyChecklistToolEvent,
|
||||||
|
isChecklistProgressTool,
|
||||||
|
type AgentTodoItem,
|
||||||
|
} from "../../agent/todoList.js";
|
||||||
import {
|
import {
|
||||||
type CardContentSegment,
|
type CardContentSegment,
|
||||||
maskMarkdownImagesForStreaming,
|
maskMarkdownImagesForStreaming,
|
||||||
@@ -125,8 +129,13 @@ export class StreamingAgentCard {
|
|||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
durationMs: number | undefined;
|
durationMs: number | undefined;
|
||||||
}): void {
|
}): void {
|
||||||
if (isTodoWriteTool(params.toolName)) {
|
if (isChecklistProgressTool(params.toolName)) {
|
||||||
const next = parseTodoWriteInput(params.input);
|
const next = applyChecklistToolEvent(this.todos, {
|
||||||
|
toolName: params.toolName,
|
||||||
|
input: params.input,
|
||||||
|
result: params.result,
|
||||||
|
toolUseId: params.toolUseId,
|
||||||
|
});
|
||||||
if (next !== null) this.todos = next;
|
if (next !== null) this.todos = next;
|
||||||
}
|
}
|
||||||
recordToolUseEnd({ runId: this.runId, ...params });
|
recordToolUseEnd({ runId: this.runId, ...params });
|
||||||
|
|||||||
@@ -581,7 +581,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
card.onToolEnd({
|
card.onToolEnd({
|
||||||
toolName: event.toolName,
|
toolName: event.toolName,
|
||||||
toolUseId: event.toolUseId,
|
toolUseId: event.toolUseId,
|
||||||
input: undefined,
|
input: event.input,
|
||||||
result: event.result,
|
result: event.result,
|
||||||
error: event.isError ? event.result : undefined,
|
error: event.isError ? event.result : undefined,
|
||||||
durationMs: event.durationMs,
|
durationMs: event.durationMs,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
applyChecklistToolEvent,
|
||||||
isTodoWriteTool,
|
isTodoWriteTool,
|
||||||
parseTodoWriteInput,
|
parseTodoWriteInput,
|
||||||
todoProgressSummary,
|
todoProgressSummary,
|
||||||
@@ -16,17 +17,49 @@ describe("todo list parse", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(todos).toEqual([
|
expect(todos).toEqual([
|
||||||
{ content: "搜题", status: "completed", activeForm: "正在搜题" },
|
{ id: undefined, content: "搜题", status: "completed", activeForm: "正在搜题" },
|
||||||
{ content: "写报告", status: "in_progress", activeForm: "正在写报告" },
|
{ id: undefined, content: "写报告", status: "in_progress", activeForm: "正在写报告" },
|
||||||
{ content: "发卡片", status: "pending", activeForm: undefined },
|
{ id: undefined, content: "发卡片", status: "pending", activeForm: undefined },
|
||||||
]);
|
]);
|
||||||
expect(todoProgressSummary(todos!)).toEqual({ completed: 1, total: 3, inProgress: 1 });
|
expect(todoProgressSummary(todos!)).toEqual({ completed: 1, total: 3, inProgress: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects empty or invalid bodies", () => {
|
it("folds TaskCreate + TaskUpdate into a checklist", () => {
|
||||||
expect(parseTodoWriteInput(null)).toBeNull();
|
let todos = applyChecklistToolEvent([], {
|
||||||
expect(parseTodoWriteInput({ todos: [] })).toBeNull();
|
toolName: "TaskCreate",
|
||||||
expect(parseTodoWriteInput({ todos: [{ status: "pending" }] })).toBeNull();
|
input: { subject: "说你好", description: "greet" },
|
||||||
|
result: "Task #1 created successfully: 说你好",
|
||||||
|
});
|
||||||
|
expect(todos).toEqual([
|
||||||
|
{ id: "1", content: "说你好", status: "pending", activeForm: undefined },
|
||||||
|
]);
|
||||||
|
|
||||||
|
todos = applyChecklistToolEvent(todos!, {
|
||||||
|
toolName: "TaskCreate_1",
|
||||||
|
input: { subject: "算 1+1", description: "math" },
|
||||||
|
result: "Task #2 created successfully: 算 1+1",
|
||||||
|
});
|
||||||
|
todos = applyChecklistToolEvent(todos!, {
|
||||||
|
toolName: "TaskUpdate",
|
||||||
|
input: { taskId: "1", status: "in_progress", activeForm: "正在问好" },
|
||||||
|
result: "Updated task #1 status",
|
||||||
|
});
|
||||||
|
todos = applyChecklistToolEvent(todos!, {
|
||||||
|
toolName: "TaskUpdate_4",
|
||||||
|
input: { taskId: "1", status: "completed" },
|
||||||
|
result: "Updated task #1 status",
|
||||||
|
});
|
||||||
|
todos = applyChecklistToolEvent(todos!, {
|
||||||
|
toolName: "TaskUpdate",
|
||||||
|
input: { taskId: "2", status: "completed" },
|
||||||
|
result: "Updated task #2 status",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(todos).toEqual([
|
||||||
|
{ id: "1", content: "说你好", status: "completed", activeForm: "正在问好" },
|
||||||
|
{ id: "2", content: "算 1+1", status: "completed", activeForm: undefined },
|
||||||
|
]);
|
||||||
|
expect(todoProgressSummary(todos!)).toEqual({ completed: 2, total: 2, inProgress: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("recognizes TodoWrite tool names", () => {
|
it("recognizes TodoWrite tool names", () => {
|
||||||
@@ -43,15 +76,15 @@ describe("agent card todo panel", () => {
|
|||||||
text: "",
|
text: "",
|
||||||
reasoningText: undefined,
|
reasoningText: undefined,
|
||||||
todos: [
|
todos: [
|
||||||
{ content: "A", status: "completed", activeForm: undefined },
|
{ id: "1", content: "A", status: "completed", activeForm: undefined },
|
||||||
{ content: "B", status: "in_progress", activeForm: "Doing B" },
|
{ id: "2", content: "B", status: "in_progress", activeForm: "Doing B" },
|
||||||
{ content: "C", status: "pending", activeForm: undefined },
|
{ id: "3", content: "C", status: "pending", activeForm: undefined },
|
||||||
],
|
],
|
||||||
toolUseSteps: [
|
toolUseSteps: [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
seq: 1,
|
seq: 1,
|
||||||
toolName: "TodoWrite",
|
toolName: "TaskCreate",
|
||||||
toolUseId: "t1",
|
toolUseId: "t1",
|
||||||
input: {},
|
input: {},
|
||||||
result: undefined,
|
result: undefined,
|
||||||
@@ -72,5 +105,7 @@ describe("agent card todo panel", () => {
|
|||||||
expect(json).toContain("任务进度 1/3");
|
expect(json).toContain("任务进度 1/3");
|
||||||
expect(json).toContain("Doing B");
|
expect(json).toContain("Doing B");
|
||||||
expect(json).toContain("collapsible_panel");
|
expect(json).toContain("collapsible_panel");
|
||||||
|
// TaskCreate noise filtered when checklist present (no separate tool panel if only Task*)
|
||||||
|
expect(json).not.toContain("TaskCreate");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user