Tool Call Events
Tổng Quan
Khi agent cần gọi công cụ (tool), nó phát ra một cặp block:
tool_use— thông tin gọi tool (tên + input)tool_result— kết quả sau khi tool thực thi xong
Hai block liên kết với nhau qua tool_use_id.
Thứ Tự Events
content_block_start (type: "tool_use", id: "toolu_01", input đầy đủ)
content_block_stop
← tool đang thực thi ở server...
content_block_start (type: "tool_result", tool_use_id: "toolu_01", content + status)
content_block_stop
Lưu ý quan trọng:
tool_use: input được load đầy đủ trongcontent_block_start, không có deltatool_result: content được load đầy đủ trongcontent_block_start, không có delta
Ví Dụ Đầy Đủ Một Tool Call
1. Server bắt đầu gọi tool
{
"type": "content_block_start",
"index": 2,
"content_block": {
"type": "tool_use",
"id": "toolu_01XyzAbc",
"name": "search_stock",
"input": {
"symbol": "VNM"
},
"tool_content_message": "Tìm kiếm cổ phiếu VNM"
},
"message_id": "msg-abc"
}
2. Tool call hoàn tất
{
"type": "content_block_stop",
"index": 2
}
Tool bắt đầu thực thi phía server...
3. Trả kết quả tool — thành công
{
"type": "content_block_start",
"index": 3,
"content_block": {
"type": "tool_result",
"tool_use_id": "toolu_01XyzAbc",
"tool_name": "search_stock",
"content": "VNM - Vinamilk\nGiá: 82,000 VND\nThay đổi: -1.2% (-1,000 VND)\nKhối lượng: 1,234,567 cổ",
"status": "success",
"artifact": null
}
}
{
"type": "content_block_stop",
"index": 3
}
4. Trả kết quả tool — lỗi
{
"type": "content_block_start",
"index": 3,
"content_block": {
"type": "tool_result",
"tool_use_id": "toolu_01XyzAbc",
"tool_name": "search_stock",
"content": "Error: Symbol VNM not found or API unavailable",
"status": "error",
"artifact": null
}
}
5. Trả kết quả tool — bị hủy (user stopped)
{
"type": "content_block_start",
"index": 3,
"content_block": {
"type": "tool_result",
"tool_use_id": "toolu_01XyzAbc",
"tool_name": "search_stock",
"content": "",
"status": "cancelled",
"artifact": null
}
}
Giá Trị tool_result.status
status | Ý nghĩa | UI |
|---|---|---|
"success" | Tool thực thi thành công | toolResultError = false → dot xanh |
"error" | Tool thất bại, có lỗi | toolResultError = true → dot đỏ |
"cancelled" | Tool bị hủy do user dừng | toolResultError = true → dot đỏ |
Logic mapping (từ production frontend):
// Dùng khi nhận content_block_stop của tool_result
toolUseBlock.toolResult = toolResultBlock.content;
toolUseBlock.toolResultError = toolResultBlock.status === 'error' || toolResultBlock.status === 'cancelled';
if (toolResultBlock.artifact) {
toolUseBlock.artifact = toolResultBlock.artifact;
}
Nhiều Tool Calls Song Song
Claude có thể gọi nhiều tool trong cùng một lượt. Mỗi tool có index riêng.
Ví Dụ: 2 Tool Calls Song Song
{ "type": "content_block_start", "index": 1, "content_block": { "type": "tool_use", "id": "toolu_01", "name": "search_stock", "input": { "symbol": "VNM" } } }
{ "type": "content_block_stop", "index": 1 }
{ "type": "content_block_start", "index": 2, "content_block": { "type": "tool_use", "id": "toolu_02", "name": "search_stock", "input": { "symbol": "HPG" } } }
{ "type": "content_block_stop", "index": 2 }
{ "type": "content_block_start", "index": 3, "content_block": { "type": "tool_result", "tool_use_id": "toolu_01", "content": "VNM: 82,000 VND", "status": "success" } }
{ "type": "content_block_stop", "index": 3 }
{ "type": "content_block_start", "index": 4, "content_block": { "type": "tool_result", "tool_use_id": "toolu_02", "content": "HPG: 28,500 VND", "status": "success" } }
{ "type": "content_block_stop", "index": 4 }
Quy tắc ghép: tool_use.id == tool_result.tool_use_id
Ghép tool_use + tool_result
Client phải merge hai block thành một object để hiển thị. Logic merge:
// Khi nhận content_block_stop của tool_result
function mergeToolResult(blocks, toolResultBlock) {
const toolUseBlock = blocks.find(b =>
b.type === 'tool_use' && b.id === toolResultBlock.tool_use_id
);
if (toolUseBlock) {
toolUseBlock.toolResult = toolResultBlock.content;
toolUseBlock.toolResultError =
toolResultBlock.status === 'error' || toolResultBlock.status === 'cancelled';
if (toolResultBlock.artifact) {
toolUseBlock.artifact = toolResultBlock.artifact;
}
}
}
Trạng Thái Hiển Thị Tool Call
Các Trạng Thái dotStatus
dotStatus | Điều kiện | UI |
|---|---|---|
active | Chưa có toolResult VÀ không phải from history | Spinner / loading dot vàng |
error | toolResultError === true | Dot đỏ |
done | Có toolResult VÀ toolResultError === false | Dot xanh |
Khi Nào Tool Không Có Result (Bị Dừng)
Khi nhận terminal_user_stopped hoặc terminal_error, các tool_use chưa có toolResult là bị dừng giữa chừng. Logic xử lý:
// Sau khi nhận terminal block
const hasTerminal = blocks.some(b =>
b.type === 'terminal_user_stopped' || b.type === 'terminal_error'
);
if (hasTerminal) {
for (const block of blocks) {
block.isStreaming = false;
// tool_use không có result → bị dừng, dùng dotStatus = 'done' (gray via isFromHistory check)
}
}
Lưu ý: Trong production frontend, isActive được tính là !hasToolResult && !isFromHistory. Khi session bị dừng và load lại từ history, isFromHistory = true → isActive = false → dotStatus = 'done' (thay vì active).
Artifact
Một số tool trả kèm artifact — structured data (chart, table, v.v.):
{
"type": "tool_result",
"tool_use_id": "toolu_01XyzAbc",
"content": "Chart được tạo thành công",
"status": "success",
"artifact": {
"type": "chart",
"data": {
"labels": [
"T2",
"T3",
"T4"
],
"values": [
82000,
81500,
83000
]
}
}
}
Client nhận artifact qua block.artifact sau khi merge.
Frontend Display Logic — Simplize Chat (Production)
Đây là logic hiển thị chính xác từ production app Simplize Chat.
Object block Sau Khi Merge
{
type: 'tool_use',
id: 'toolu_01XyzAbc',
name: 'search_stock',
input: { symbol: 'VNM' },
tool_content_message: 'Tìm kiếm cổ phiếu VNM', // label hiển thị
toolResult: 'VNM: 82,000 VND', // null nếu chưa có
toolResultError: false, // true nếu status error/cancelled
artifact: null, // artifact data nếu có
isStreaming: false, // true khi đang stream live
isFromHistory: false, // true khi load từ history API
}
Tính dotStatus
const hasToolResult = block.toolResult !== null && block.toolResult !== undefined;
const isError = block.toolResultError;
const isActive = !hasToolResult && !isFromHistory;
const dotStatus = isActive ? 'active'
: isError ? 'error'
: 'done';
Component ToolCallBlock (Simplified)
function ToolCallBlock({ block, isFromHistory = false }) {
const toolName = block.tool_content_message || block.name;
const hasToolResult = block.toolResult !== null && block.toolResult !== undefined;
const isError = block.toolResultError;
const isActive = !hasToolResult && !isFromHistory;
const [isExpanded, setIsExpanded] = useState(!isFromHistory && isActive);
const dotStatus = isActive ? 'active' : isError ? 'error' : 'done';
return (
<div className="py-1.5">
<div
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-start gap-1.5 text-[13px] text-gray-500 cursor-pointer hover:text-gray-700"
>
<StatusDot status={dotStatus} />
<ShimmerText isStreaming={isActive}>
<span>{toolName}</span>
</ShimmerText>
</div>
{isExpanded && (
<div className="mt-2 space-y-3">
<div>
<div className="text-xs font-semibold text-gray-500 mb-1">Request</div>
<pre className="text-xs font-mono bg-gray-50 p-2 rounded text-gray-700 whitespace-pre-wrap">
{JSON.stringify(block.input, null, 2)}
</pre>
</div>
{hasToolResult && (
<div>
<div className="text-xs font-semibold text-gray-500 mb-1">Response</div>
<pre className={`text-xs font-mono p-2 rounded whitespace-pre-wrap ${isError ? 'bg-red-50 text-red-700' : 'bg-gray-50 text-gray-700'}`}>
{block.toolResult}
</pre>
</div>
)}
</div>
)}
</div>
);
}
StatusDot Component
function StatusDot({ status }) {
if (status === 'active') {
return (
<div className="w-3 h-3 mt-0.5 flex-shrink-0 rounded-full border-2 border-yellow-400 border-t-transparent animate-spin" />
);
}
const colors = {
done: 'bg-green-500',
error: 'bg-red-500',
};
return (
<div className={`w-2.5 h-2.5 mt-0.5 flex-shrink-0 rounded-full ${colors[status] ?? 'bg-gray-400'}`} />
);
}
ShimmerText — Hiệu Ứng Loading
function ShimmerText({ isStreaming, children }) {
if (!isStreaming) return <>{children}</>;
return (
<span className="animate-pulse opacity-70">
{children}
</span>
);
}
Ví Dụ: Flutter (Dart)
class ToolCallBlock {
final String id;
final String name;
final String? toolContentMessage;
final Map<String, dynamic> input;
String? toolResult;
bool toolResultError;
Map<String, dynamic>? artifact;
bool isFromHistory;
ToolCallBlock({
required this.id,
required this.name,
this.toolContentMessage,
required this.input,
this.toolResult,
this.toolResultError = false,
this.artifact,
this.isFromHistory = false,
});
ToolStatus get dotStatus {
final hasResult = toolResult != null;
final isActive = !hasResult && !isFromHistory;
if (isActive) return ToolStatus.active;
if (toolResultError) return ToolStatus.error;
return ToolStatus.done;
}
// Merge tool_result vào block này
void applyToolResult({
required String content,
required String status,
Map<String, dynamic>? artifact,
}) {
toolResult = content;
toolResultError = status == 'error' || status == 'cancelled';
if (artifact != null) {
this.artifact = artifact;
}
}
}
enum ToolStatus { active, done, error }
class ToolCallWidget extends StatefulWidget {
final ToolCallBlock block;
const ToolCallWidget({required this.block});
@override
State<ToolCallWidget> createState() => _ToolCallWidgetState();
}
class _ToolCallWidgetState extends State<ToolCallWidget> {
late bool isExpanded;
@override
void initState() {
super.initState();
isExpanded = !widget.block.isFromHistory &&
widget.block.dotStatus == ToolStatus.active;
}
@override
Widget build(BuildContext context) {
final block = widget.block;
final status = block.dotStatus;
final hasResult = block.toolResult != null;
final toolName = block.toolContentMessage ?? block.name;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => setState(() => isExpanded = !isExpanded),
child: Row(children: [
ToolStatusDot(status: status),
const SizedBox(width: 6),
Text(
toolName,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
),
]),
),
if (isExpanded) ...[
const SizedBox(height: 8),
_buildSection(
'Request',
const JsonEncoder.withIndent(' ').convert(block.input),
isError: false,
),
if (hasResult) ...[
const SizedBox(height: 8),
_buildSection(
'Response',
block.toolResult!,
isError: block.toolResultError,
),
],
],
],
);
}
Widget _buildSection(String label, String content, {required bool isError}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontSize: 11, fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isError ? Colors.red[50] : Colors.grey[50],
borderRadius: BorderRadius.circular(4),
),
child: Text(
content,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: isError ? Colors.red[700] : Colors.grey[800],
),
),
),
],
);
}
}
class ToolStatusDot extends StatelessWidget {
final ToolStatus status;
const ToolStatusDot({required this.status});
@override
Widget build(BuildContext context) {
const size = 10.0;
if (status == ToolStatus.active) {
return SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.amber[400],
),
);
}
final color = status == ToolStatus.error ? Colors.red : Colors.green;
return Container(
width: size,
height: size,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
}
Tóm Tắt Flow Tool Call
1. content_block_start (tool_use, input đầy đủ)
→ Client: hiển thị tool name + dotStatus=active (spinner)
2. content_block_stop (tool_use)
→ Client: tool đang chạy ở server, tiếp tục spinner
3. content_block_start (tool_result, content + status)
→ Client: merge vào tool_use block:
toolResult = content
toolResultError = (status === 'error' || status === 'cancelled')
artifact = artifact if exists
4. content_block_stop (tool_result)
→ Client: update dotStatus:
isError=true → dotStatus='error' (dot đỏ)
isError=false → dotStatus='done' (dot xanh)