Chuyển tới nội dung chính

Todo List

Agent sử dụng tool write_todos để tạo và cập nhật task list theo thời gian thực. Mỗi lần gọi tool là một snapshot đầy đủ của toàn bộ danh sách.


Cấu Trúc Todo

{
"todos": [
{
"content": "Lấy dữ liệu giá cổ phiếu HPG",
"activeForm": "Đang lấy dữ liệu giá cổ phiếu HPG",
"status": "completed"
},
{
"content": "Phân tích xu hướng kỹ thuật",
"activeForm": "Đang phân tích xu hướng kỹ thuật",
"status": "in_progress"
},
{
"content": "Viết báo cáo tổng hợp",
"activeForm": "Đang viết báo cáo tổng hợp",
"status": "pending"
}
]
}
FieldMô tả
contentDạng imperative — hiển thị khi pending hoặc completed
activeFormDạng present continuous — hiển thị khi in_progress
status"pending" / "in_progress" / "completed"

Ràng buộc:

  • Tối đa 1 task ở trạng thái in_progress cùng lúc
  • contentactiveForm đều không được để trống

Realtime Stream

Tool_use Event

write_todos được stream như một tool_use block. Khác với các tool thông thường, toàn bộ input được gửi đầy đủ trong content_block_start — không có content_block_delta.

{
"type": "content_block_start",
"index": 1,
"content_block": {
"type": "tool_use",
"id": "toolu_01",
"name": "write_todos",
"tool_content_message": "Update Todos",
"input": {
"todos": [
{
"content": "Lấy dữ liệu giá HPG",
"activeForm": "Đang lấy dữ liệu giá HPG",
"status": "completed"
},
{
"content": "Phân tích xu hướng",
"activeForm": "Đang phân tích xu hướng",
"status": "in_progress"
}
]
}
}
}

Tiếp theo ngay là content_block_stop:

{
"type": "content_block_stop",
"index": 1
}

Không có tool_result cho write_todos — tool không trả về kết quả.

Cập Nhật Side Panel

Frontend lắng nghe content_block_start với type === 'tool_use' và tên chứa "todo":

function handleTodoSSEEvent(data, setTodosProgress) {
if (data.type === 'content_block_start' && data.content_block?.type === 'tool_use') {
if (isTodoToolName(data.content_block.name) && !data.content_block.parent_tool_use_id) {
const todos = data.content_block.input?.todos;
if (todos && Array.isArray(todos)) {
setTodosProgress(todos);
}
}
}
}

TodoProgressPanel (sidebar phải) cập nhật ngay khi nhận event — không cần đợi content_block_stop.

parent_tool_use_id check để bỏ qua todos từ sub-agent — chỉ hiện top-level todos.


History (F5 Reload)

Todo Side Panel

Panel restore từ workspace.todos trong history response:

{
"workspace": {
"todos": [
{
"content": "Lấy dữ liệu giá HPG",
"activeForm": "Đang lấy dữ liệu giá HPG",
"status": "completed"
},
{
"content": "Phân tích xu hướng",
"activeForm": "Đang phân tích xu hướng",
"status": "completed"
},
{
"content": "Viết báo cáo",
"activeForm": "Đang viết báo cáo",
"status": "completed"
}
]
}
}
const restoredTodos = historyData?.workspace?.todos || null;
setTodosProgress(restoredTodos);

Tool_use Block Trong AI Message

Trong history, write_todos xuất hiện trong msg.content array của assistant message:

{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01",
"name": "write_todos",
"input": {
"todos": [
{
"content": "Lấy dữ liệu giá HPG",
"activeForm": "Đang lấy dữ liệu giá HPG",
"status": "completed"
},
{
"content": "Phân tích xu hướng",
"activeForm": "Đang phân tích xu hướng",
"status": "in_progress"
}
]
}
}
],
"tool_calls": []
}

parseV2History() parse thành block object:

// Kết quả blocks array của AI message
{
id: "session-history-1",
type: "ai_answer",
isStreaming: false,
blocks: [
{
type: "tool_use",
id: "toolu_01",
name: "write_todos",
input: {
todos: [
{ content: "Lấy dữ liệu giá HPG", activeForm: "...", status: "completed" },
{ content: "Phân tích xu hướng", activeForm: "...", status: "in_progress" }
]
},
tool_content_message: "Update Todos"
}
]
}

renderBlock() detect isTodoTool(block.name) → render TodoListBlock thay vì ToolCallBlock thông thường.


Frontend Display

Status Indicators

StatusIconTextStyle
completed (xanh lá)contentline-through, text-gray-400
in_progress (chấm xanh dương)activeFormBold, text-gray-800
pending (vòng tròn border)contenttext-gray-600

Render Logic

function TodoListBlock({ todos }) {
return (
<ul>
{todos.map((todo, i) => (
<li key={i} className={
todo.status === 'completed' ? 'line-through text-gray-400' :
todo.status === 'in_progress' ? 'font-medium text-gray-800' :
'text-gray-600'
}>
<StatusIcon status={todo.status} />
<span>
{todo.status === 'in_progress' ? todo.activeForm : todo.content}
</span>
</li>
))}
</ul>
);
}

function StatusIcon({ status }) {
if (status === 'completed') return <span className="text-green-500"></span>;
if (status === 'in_progress') return <span className="w-2 h-2 rounded-full bg-blue-500" />;
return <span className="w-2 h-2 rounded-full border border-gray-400" />;
}

Ví Dụ: Flutter (Dart)

class TodoListWidget extends StatelessWidget {
final List<Map<String, dynamic>> todos;
const TodoListWidget({required this.todos});

@override
Widget build(BuildContext context) {
return Column(
children: todos.map((todo) {
final status = todo['status'] as String;
final text = status == 'in_progress'
? todo['activeForm'] as String
: todo['content'] as String;

return Row(
children: [
_StatusIcon(status: status),
const SizedBox(width: 8),
Text(
text,
style: TextStyle(
decoration: status == 'completed' ? TextDecoration.lineThrough : null,
color: status == 'in_progress' ? Colors.grey[900] : Colors.grey[600],
fontWeight: status == 'in_progress' ? FontWeight.w600 : null,
),
),
],
);
}).toList(),
);
}
}

class _StatusIcon extends StatelessWidget {
final String status;
const _StatusIcon({required this.status});

@override
Widget build(BuildContext context) {
if (status == 'completed') {
return const Icon(Icons.check_circle, color: Colors.green, size: 16);
}
if (status == 'in_progress') {
return Container(
width: 8, height: 8,
decoration: const BoxDecoration(color: Colors.blue, shape: BoxShape.circle),
);
}
return Container(
width: 8, height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey),
),
);
}
}