Files
CodePilot/docs/handover/agent-tooling-todo-bridge.md
7418andClaude Opus 4.6 6209c865fd refactor: 整理 docs 目录结构
- docs/handover/ — 交接文档(架构、设计决策)
- docs/research/ — 调研文档(技术方案、可行性分析)
- 各目录添加 README.md 索引,要求 AI 增删文件时更新索引
- CLAUDE.md 更新文档路径引用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:41:14 +08:00

4.1 KiB

Agent Tooling & TodoWrite Bridge

Architecture Overview

SDK TodoWrite tool_use
  → PostToolUse hook (claude-client.ts)
    → Emits tool_result SSE (deduped)
    → Emits task_update SSE { session_id, todos[] }
      → collectStreamResponse (route.ts)
        → syncSdkTasks() persists to DB
      → Frontend SSE consumer (useSSEStream.ts)
        → onTaskUpdate dispatches 'tasks-updated' event
          → TaskList re-fetches from /api/tasks

TodoWrite Field Mapping

SDK TodoWriteInput field Local tasks table column Notes
(array index) id (prefixed sdk-{sessionId}-{index}) Full sessionId avoids collision
content title Main display text
status status Mapped via mapStatus()
activeForm description Present-continuous form label
(array index) sort_order Preserves original order

Status Mapping

SDK status Local TaskStatus
pending pending
in_progress in_progress
completed completed
Other pending

Idempotency

syncSdkTasks() uses a replace-all strategy within a transaction:

  1. DELETE FROM tasks WHERE session_id = ? AND source = 'sdk'
  2. INSERT all todos from the latest TodoWrite payload

This is naturally idempotent — calling it multiple times with the same data produces identical results. User-created tasks (source = 'user') are never affected.

tool_result Two-Layer Last-Wins Dedup

Both PostToolUse hook and user message handler in the SDK can emit the same tool_result. PostToolUse fires first (for immediate UI feedback); the user handler's result may be more complete/canonical. Strategy: both sources emit freely; consumption layers use "last wins" (replace, not skip) so the most complete result is always kept.

Layer 1: route.ts (DB persistence)

  • seenToolResultIds Set in collectStreamResponse() — when a duplicate tool_use_id arrives, the existing contentBlock entry is replaced with the newer result.

Layer 2: ChatView.tsx (UI state)

  • onToolResult finds existing entry by tool_use_id and replaces it instead of appending a duplicate.

SSE Event Types

Event Source Purpose
text stream_event Streaming text delta
tool_use assistant message Tool invocation
tool_result PostToolUse / user Tool execution result (deduped)
tool_output stderr callback Raw tool output
tool_timeout tool_progress Tool execution timeout
task_update PostToolUse TodoWrite sync trigger
status system message Session init, notifications
result result message Final result with usage stats
permission_request canUseTool Permission approval needed
mode_changed system status SDK mode change
error catch block Error occurred
done stream end Stream complete

Troubleshooting

Tasks not appearing after TodoWrite

  1. Check server console for [claude-client] logs — ensure PostToolUse fires
  2. Verify task_update SSE event in browser DevTools Network tab
  3. Check syncSdkTasks() runs in collectStreamResponse (server-side tee'd stream)
  4. Verify tasks-updated CustomEvent fires in browser console

Duplicate tool results

  1. Check emittedToolResultIds Set size in server logs
  2. Compare SSE event count before/after (should be halved for tools)
  3. Query messages table: SELECT content FROM messages WHERE content LIKE '%tool_result%' — each tool_use_id should appear once

SDK upgrade issues

  • Current: v0.2.62 (upgraded from v0.2.33)
  • Rollback: npm install @anthropic-ai/claude-agent-sdk@0.2.33
  • Key integration points: canUseTool, PostToolUse, Notification, tool_progress, system, result