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"
}
]
}
| Field | Mô tả |
|---|---|
content | Dạng imperative — hiển thị khi pending hoặc completed |
activeForm | Dạ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_progresscùng lúc contentvàactiveFormđề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_resultchowrite_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_idcheck để 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
| Status | Icon | Text | Style |
|---|---|---|---|
completed | ✓ (xanh lá) | content | line-through, text-gray-400 |
in_progress | • (chấm xanh dương) | activeForm | Bold, text-gray-800 |
pending | ○ (vòng tròn border) | content | text-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),
),
);
}
}