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

Đị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"
}
FieldKiểuMô tả
session_idstringSession ID của lượt này
timestampfloatUnix timestamp khi bắt đầu
display_modestring"agent" hoặc "chat"
is_new_session_from_shareboolSession tạo từ share link
message_idstringID 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ự:

EventMô tả
content_block_startBắt đầu block, kèm metadata
content_block_deltaNội dung dần dần (lặp lại nhiều lần)
content_block_stopBlock 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
}
}
FieldMô tả
id / tool_use_idID duy nhất của lần gọi tool
nameTên hàm tool
tool_content_messageTên hiển thị thân thiện
inputArguments đầy đủ
parent_tool_use_idID tool cha khi dùng sub-agent

Stop:

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

Không có content_block_delta cho tool_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
}
}
FieldMô tả
tool_use_idKhớp với id của tool_use tương ứng
status"success" hoặc "error"
contentKết quả (tối đa 500 ký tự)
artifactDữ liệu phụ: chart, table, file (nếu có)

Stop:

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

Không có content_block_delta cho tool_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", đổi block.type thà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", đổi block.type thà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,
);
}
}