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,
);
}
}