Định Dạng Event Stream
Tất cả events là JSON object gửi qua WebSocket text frame. Mọi event đều có trường type.
Cấu Trúc Envelope
Mỗi event từ agent output đều có thêm message_id:
{
"type": "content_block_delta",
"index": 1,
"delta": {
"type": "text_delta",
"text": "Phân tích cho thấy..."
},
"message_id": "msg-uuid-abc"
}
message_id là ID của lượt xử lý, dùng để group các events thuộc cùng 1 lượt.
Khung Lượt (Turn Envelope)
Mỗi lượt agent trả lời luôn theo thứ tự này:
message_start
content_block_start (index: 0)
content_block_delta ×N
content_block_stop
content_block_start (index: 1)
...
message_delta
message_stop
message_start
{
"type": "message_start",
"session_id": "abc-123",
"timestamp": 1710000000.123,
"display_mode": "agent",
"is_new_session_from_share": false,
"message_id": "msg-uuid-abc"
}
| Field | Kiểu | Mô tả |
|---|---|---|
session_id | string | Session ID của lượt này |
timestamp | float | Unix timestamp khi bắt đầu |
display_mode | string | "agent" hoặc "chat" |
is_new_session_from_share | bool | Session tạo từ share link |
message_id | string | ID duy nhất của lượt này |
message_delta
{
"type": "message_delta",
"delta": {
"stop_reason": "end_turn"
}
}
stop_reason: "end_turn" — agent hoàn thành bình thường.
message_stop
{
"type": "message_stop",
"duration_ms": 3420,
"message_id": "msg-uuid-abc"
}
Content Block Events
Mỗi block nội dung gồm 3 events theo thứ tự:
| Event | Mô tả |
|---|---|
content_block_start | Bắt đầu block, kèm metadata |
content_block_delta | Nội dung dần dần (lặp lại nhiều lần) |
content_block_stop | Block kết thúc |
index là số nguyên 0-based tăng dần qua tất cả blocks trong 1 lượt.
Block: thinking
Chuỗi suy luận của AI. Là block đầu tiên, chỉ có khi display_mode: "agent".
Start:
{
"type": "content_block_start",
"index": 0,
"content_block": {
"type": "thinking",
"thinking": "",
"subagent": null,
"parent_tool_use_id": null
}
}
Delta (lặp lại nhiều lần, nối delta.thinking lại):
{
"type": "content_block_delta",
"index": 0,
"delta": {
"type": "thinking_delta",
"thinking": "Tôi cần tra giá cổ phiếu VNM..."
}
}
Stop:
{
"type": "content_block_stop",
"index": 0
}
Block: text
Câu trả lời văn bản chính của AI. Nối tất cả delta.text để có nội dung đầy đủ.
Start:
{
"type": "content_block_start",
"index": 1,
"content_block": {
"type": "text",
"text": "",
"subagent": null,
"parent_tool_use_id": null,
"is_part": false
}
}
Delta:
{ "type": "content_block_delta", "index": 1, "delta": { "type": "text_delta", "text": "Phân tích cổ phiếu VNM:\n\n" } }
{ "type": "content_block_delta", "index": 1, "delta": { "type": "text_delta", "text": "Giá hiện tại: **82,000 VND**" } }
Stop:
{
"type": "content_block_stop",
"index": 1,
"is_final": true
}
is_final: true khi đây là câu trả lời cuối (không còn tool calls tiếp theo).
Block: tool_use
Agent gọi một công cụ. Input có sẵn ngay trong content_block_start, không có delta.
Start:
{
"type": "content_block_start",
"index": 2,
"content_block": {
"type": "tool_use",
"id": "toolu_01XyzAbc",
"name": "search_stock",
"tool_use_id": "toolu_01XyzAbc",
"tool_content_message": "Tìm kiếm cổ phiếu",
"input": {
"symbol": "VNM"
},
"subagent": null,
"parent_tool_use_id": null
}
}
| Field | Mô tả |
|---|---|
id / tool_use_id | ID duy nhất của lần gọi tool |
name | Tên hàm tool |
tool_content_message | Tên hiển thị thân thiện |
input | Arguments đầy đủ |
parent_tool_use_id | ID tool cha khi dùng sub-agent |
Stop:
{
"type": "content_block_stop",
"index": 2
}
Không có
content_block_deltachotool_use.
Block: tool_result
Kết quả sau khi tool thực thi. Nội dung có sẵn trong content_block_start, không có delta.
Start:
{
"type": "content_block_start",
"index": 3,
"content_block": {
"type": "tool_result",
"tool_use_id": "toolu_01XyzAbc",
"name": "search_stock",
"status": "success",
"content": "VNM - Vinamilk\nGiá: 82,000 VND\nThay đổi: -1.2%\nKhối lượng: 1.2M",
"artifact": null,
"subagent": null,
"parent_tool_use_id": null
}
}
| Field | Mô tả |
|---|---|
tool_use_id | Khớp với id của tool_use tương ứng |
status | "success" hoặc "error" |
content | Kết quả (tối đa 500 ký tự) |
artifact | Dữ liệu phụ: chart, table, file (nếu có) |
Stop:
{
"type": "content_block_stop",
"index": 3
}
Không có
content_block_deltachotool_result.
Block: terminal_user_stopped
Server gửi dưới dạng block text thông thường với trường extras trong delta. Client phải tự chuyển đổi type.
Start:
{
"type": "content_block_start",
"index": 4,
"content_block": {
"type": "text",
"text": ""
}
}
Delta:
{
"type": "content_block_delta",
"index": 4,
"delta": {
"type": "text_delta",
"text": "Người dùng đã dừng cuộc trò chuyện. Gửi tin nhắn mới để tiếp tục",
"extras": {
"block_subtype": "user_stopped"
}
}
}
Xử lý: Khi nhận
delta.extras.block_subtype === "user_stopped", đổiblock.typethành"terminal_user_stopped".
Block: terminal_error
Tương tự terminal_user_stopped nhưng với block_subtype: "error".
Delta:
{
"type": "content_block_delta",
"index": 4,
"delta": {
"type": "text_delta",
"text": "Đã xảy ra lỗi. Vui lòng thử lại.",
"extras": {
"block_subtype": "error",
"code": "LLM_ERROR",
"can_retry": true,
"error_type": "terminal",
"details": {
"error": "Rate limit exceeded"
}
}
}
}
Xử lý: Khi nhận
delta.extras.block_subtype === "error", đổiblock.typethành"terminal_error".
Block: file_processing
Xuất hiện khi có content_urls trong chat. Luôn là block đầu tiên (index 0).
Start:
{
"type": "content_block_start",
"index": 0,
"content_block": {
"type": "file_processing",
"status": "processing",
"files": [
{
"url": "https://example.com/bao-cao.pdf"
}
]
}
}
Delta (cập nhật trạng thái):
{
"type": "content_block_delta",
"index": 0,
"delta": {
"status": "completed",
"message": "Processed 1 file"
}
}
Stop:
{
"type": "content_block_stop",
"index": 0
}
Block: approval_request
Xem Phê duyệt (HITL) để biết đầy đủ.
Start:
{
"type": "content_block_start",
"index": 5,
"content_block": {
"type": "approval_request",
"approval_key": "abc-123_1"
}
}
Delta:
{
"type": "content_block_delta",
"index": 5,
"delta": {
"action_requests": [
{
"name": "execute_trade",
"args": {
"symbol": "VNM",
"quantity": 100
}
}
],
"review_configs": [
{
"require_approval": true
}
],
"timeout_seconds": 300
}
}
Ví Dụ Đầy Đủ: Một Lượt Hoàn Chỉnh
Lượt agent trả lời về VNM với 1 tool call:
{ "type": "message_start", "session_id": "abc-123", "timestamp": 1710000000.0, "display_mode": "agent", "message_id": "msg-001" }
{ "type": "content_block_start", "index": 0, "content_block": { "type": "thinking", "thinking": "" } }
{ "type": "content_block_delta", "index": 0, "delta": { "type": "thinking_delta", "thinking": "Cần tra giá VNM trước." } }
{ "type": "content_block_stop", "index": 0 }
{ "type": "content_block_start", "index": 1, "content_block": { "type": "tool_use", "id": "toolu_01", "name": "search_stock", "tool_use_id": "toolu_01", "tool_content_message": "Tìm kiếm cổ phiếu", "input": { "symbol": "VNM" } } }
{ "type": "content_block_stop", "index": 1 }
{ "type": "content_block_start", "index": 2, "content_block": { "type": "tool_result", "tool_use_id": "toolu_01", "name": "search_stock", "status": "success", "content": "VNM: 82,000 VND (-1.2%)" } }
{ "type": "content_block_stop", "index": 2 }
{ "type": "content_block_start", "index": 3, "content_block": { "type": "text", "text": "" } }
{ "type": "content_block_delta", "index": 3, "delta": { "type": "text_delta", "text": "Cổ phiếu **VNM** đang giao dịch ở **82,000 VND**, giảm 1.2%." } }
{ "type": "content_block_stop", "index": 3, "is_final": true }
{ "type": "message_delta", "delta": { "stop_reason": "end_turn" } }
{ "type": "message_stop", "duration_ms": 2840, "message_id": "msg-001" }
Ví Dụ: React.js — Xử Lý Event Stream
function useStreamProcessor() {
const [blocks, setBlocks] = useState([])
const [isStreaming, setIsStreaming] = useState(false)
const inProgress = useRef(new Map()) // index → block đang stream
const toolUseMap = useRef(new Map()) // tool_use id → index trong blocks array
const handleEvent = useCallback((msg) => {
switch (msg.type) {
case 'message_start':
inProgress.current.clear()
toolUseMap.current.clear()
setBlocks([])
setIsStreaming(true)
break
case 'content_block_start': {
const cb = msg.content_block
inProgress.current.set(msg.index, {
index: msg.index,
...cb,
_parts: [],
})
break
}
case 'content_block_delta': {
const block = inProgress.current.get(msg.index)
if (!block) break
const d = msg.delta
// Text / thinking
if (d.type === 'text_delta') {
block._parts.push(d.text ?? '')
block.text = block._parts.join('')
// Phát hiện terminal blocks
if (d.extras?.block_subtype === 'user_stopped') {
block.type = 'terminal_user_stopped'
} else if (d.extras?.block_subtype === 'error') {
block.type = 'terminal_error'
block.extras = d.extras
}
} else if (d.type === 'thinking_delta') {
block._parts.push(d.thinking ?? '')
block.thinking = block._parts.join('')
}
// approval_request delta
if (d.action_requests) {
block.actionRequests = d.action_requests
block.reviewConfigs = d.review_configs
block.timeoutSeconds = d.timeout_seconds
}
// file_processing status delta
if (d.status) block.processingStatus = d.status
if (d.message) block.processingMessage = d.message
break
}
case 'content_block_stop': {
const block = inProgress.current.get(msg.index)
if (!block) break
inProgress.current.delete(msg.index)
if (block.type === 'tool_result') {
// Ghép kết quả vào tool_use block tương ứng
const idx = toolUseMap.current.get(block.tool_use_id)
if (idx != null) {
setBlocks(prev => prev.map((b, i) =>
i === idx
? { ...b, toolResult: block.content, toolResultError: block.status === 'error', artifact: block.artifact }
: b
))
break
}
}
setBlocks(prev => {
const next = [...prev, block]
if (block.type === 'tool_use') {
toolUseMap.current.set(block.id, next.length - 1)
}
return next
})
break
}
case 'message_stop':
setIsStreaming(false)
break
}
}, [])
return { blocks, isStreaming, handleEvent }
}
Ví Dụ: Flutter (Dart) — Xử Lý Event Stream
class StreamProcessor extends ChangeNotifier {
final _inProgress = <int, ContentBlock>{};
final _toolUseIndexes = <String, int>{};
List<ContentBlock> blocks = [];
bool isStreaming = false;
void handleEvent(Map<String, dynamic> msg) {
switch (msg['type'] as String) {
case 'message_start':
_inProgress.clear();
_toolUseIndexes.clear();
blocks = [];
isStreaming = true;
notifyListeners();
case 'content_block_start':
final cb = Map<String, dynamic>.from(msg['content_block'] as Map);
_inProgress[msg['index'] as int] = ContentBlock(
index: msg['index'] as int,
type: cb['type'] as String,
data: cb,
);
case 'content_block_delta':
final block = _inProgress[msg['index'] as int];
if (block == null) break;
final delta = msg['delta'] as Map<String, dynamic>;
if (delta['type'] == 'text_delta') {
block.text = (block.text ?? '') + (delta['text'] as String? ?? '');
final extras = delta['extras'] as Map?;
if (extras?['block_subtype'] == 'user_stopped') {
block.type = 'terminal_user_stopped';
} else if (extras?['block_subtype'] == 'error') {
block.type = 'terminal_error';
block.extras = Map<String, dynamic>.from(extras!);
}
} else if (delta['type'] == 'thinking_delta') {
block.thinking = (block.thinking ?? '') + (delta['thinking'] as String? ?? '');
}
if (delta['action_requests'] != null) {
block.actionRequests = delta['action_requests'] as List;
}
blocks = [..._inProgress.values.toList()..sort((a, b) => a.index - b.index)];
notifyListeners();
case 'content_block_stop':
final block = _inProgress.remove(msg['index'] as int);
if (block == null) break;
if (block.type == 'tool_result') {
final idx = _toolUseIndexes[block.toolUseId];
if (idx != null) {
blocks = List.from(blocks);
blocks[idx] = blocks[idx].copyWith(
toolResult: block.data['content'] as String?,
toolResultError: block.data['status'] == 'error',
);
notifyListeners();
break;
}
}
if (block.type == 'tool_use') {
_toolUseIndexes[block.data['id'] as String] = blocks.length;
}
blocks = [...blocks, block];
notifyListeners();
case 'message_stop':
isStreaming = false;
notifyListeners();
}
}
}
class ContentBlock {
int index;
String type;
Map<String, dynamic> data;
String? text;
String? thinking;
String? toolUseId;
String? toolResult;
bool toolResultError;
Map<String, dynamic>? extras;
List? actionRequests;
ContentBlock({
required this.index,
required this.type,
required this.data,
this.text,
this.thinking,
this.toolUseId,
this.toolResult,
this.toolResultError = false,
this.extras,
this.actionRequests,
}) {
toolUseId = data['tool_use_id'] as String?;
}
ContentBlock copyWith({String? toolResult, bool? toolResultError}) {
return ContentBlock(
index: index, type: type, data: data,
text: text, thinking: thinking, toolUseId: toolUseId,
toolResult: toolResult ?? this.toolResult,
toolResultError: toolResultError ?? this.toolResultError,
);
}
}