Luồng Phê Duyệt (Human-in-the-Loop)
Tổng Quan
Một số tool calls yêu cầu xác nhận của người dùng trước khi thực thi. Khi đó agent sẽ:
- Dừng lại (pause)
- Gửi block
approval_requestvới danh sách actions cần phê duyệt - Chờ client gửi lệnh
approvalvới quyết định - Tiếp tục hoặc dừng tùy theo quyết định
Luồng Đầy Đủ
Server Client
│ │
│── content_block_start (approval_request)▶│
│── content_block_stop ───────────────────▶│
│ │
│ (agent đang chờ...) │ ← hiển thị UI confirm
│ │
│◀── approval {decisions: [...]} ──────────│
│ │
│── content_block_start (approval_result) ▶│
│── content_block_delta {decisions} ──────▶│
│── content_block_stop ───────────────────▶│
│ │
│── (tiếp tục chạy agent nếu approved) ───▶│
Block approval_request
Ví Dụ Đầy Đủ
{
"type": "content_block_start",
"index": 4,
"content_block": {
"type": "approval_request",
"approval_key": "abc-123_1",
"actions": [
{
"name": "execute_trade",
"args": {
"symbol": "VNM",
"quantity": 100,
"side": "buy",
"price": 82000
},
"tool_use_id": "toolu_01XyzAbc"
}
],
"review_configs": [
{
"require_approval": true,
"timeout": 300
}
]
},
"message_id": "msg-abc"
}
{
"type": "content_block_stop",
"index": 4
}
Giải Thích Trường
| Trường | Kiểu | Mô tả |
|---|---|---|
approval_key | string | Key duy nhất, format {session_id}_{interrupt_count} |
actions | array | Danh sách actions cần phê duyệt |
actions[].name | string | Tên tool |
actions[].args | object | Arguments đầy đủ của tool |
actions[].tool_use_id | string | ID của block tool_use tương ứng |
review_configs[].timeout | number | Giây chờ trước khi auto-reject (mặc định 300) |
Lệnh approval — Phản Hồi Quyết Định
Phê Duyệt
{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "approve"
}
]
}
Từ Chối
{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "reject"
}
]
}
Sửa Arguments Trước Khi Thực Thi
{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "edit",
"edited_action": {
"name": "execute_trade",
"args": {
"symbol": "VNM",
"quantity": 50,
"side": "buy",
"price": 82000
}
}
}
]
}
Kèm Nội Dung Text Từ Người Dùng
{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "approve"
}
],
"user_edit_content": "OK nhưng chỉ mua 50 cổ thôi"
}
Nhiều Actions
Nếu actions có 2 phần tử, decisions cần đủ 2 phần tử:
{
"decisions": [
{
"type": "approve"
},
{
"type": "reject"
}
]
}
Nếu decisions ít hơn actions, phần còn thiếu tự động fill bằng decision đầu tiên.
Block approval_result — Kết Quả Sau Quyết Định
{
"type": "content_block_start",
"index": 5,
"content_block": {
"type": "approval_result",
"approval_key": "abc-123_1"
}
}
{
"type": "content_block_delta",
"index": 5,
"delta": {
"decisions": [
{ "type": "approve" }
]
}
}
{ "type": "content_block_stop", "index": 5 }
Block approval_timeout — Hết Thời Gian
Nếu user không phản hồi trong timeout giây, tất cả actions bị auto-reject:
{ "type": "content_block_start", "index": 5, "content_block": { "type": "approval_timeout", "approval_key": "abc-123_1" } }
{ "type": "content_block_stop", "index": 5 }
Ví Dụ: React.js
function ApprovalModal({ request, sessionId, onSend }) {
const [decisions, setDecisions] = useState(
request.actions.map(() => 'approve')
)
const submit = () => {
onSend({
type: 'approval',
session_id: sessionId,
approval_key: request.approval_key,
decisions: decisions.map(type => ({ type })),
})
}
return (
<div className="modal">
<h3>Xác nhận hành động</h3>
{request.actions.map((action, i) => (
<div key={i} className="action-item">
<h4>{action.name}</h4>
<pre>{JSON.stringify(action.args, null, 2)}</pre>
<div className="decision-buttons">
<button
className={decisions[i] === 'approve' ? 'active' : ''}
onClick={() => {
const next = [...decisions]
next[i] = 'approve'
setDecisions(next)
}}
>
Phê duyệt
</button>
<button
className={decisions[i] === 'reject' ? 'active' : ''}
onClick={() => {
const next = [...decisions]
next[i] = 'reject'
setDecisions(next)
}}
>
Từ chối
</button>
</div>
</div>
))}
<button onClick={submit}>Xác nhận</button>
</div>
)
}
// Trong event handler
function handleEvent(msg) {
if (msg.type === 'content_block_start' &&
msg.content_block.type === 'approval_request') {
setApprovalRequest(msg.content_block)
}
}
Ví Dụ: Flutter (Dart)
void showApprovalDialog(
BuildContext context,
Map<String, dynamic> approvalBlock,
String sessionId,
Function(Map<String, dynamic>) sendApproval,
) {
final actions = (approvalBlock['actions'] as List).cast<Map<String, dynamic>>();
final decisions = List<String>.filled(actions.length, 'approve');
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Xác nhận hành động'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: actions.asMap().entries.map((e) {
final i = e.key;
final action = e.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(action['name'] as String,
style: const TextStyle(fontWeight: FontWeight.bold)),
Text(const JsonEncoder.withIndent(' ')
.convert(action['args']),
style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
Row(children: [
ChoiceChip(
label: const Text('Phê duyệt'),
selected: decisions[i] == 'approve',
onSelected: (_) => setState(() => decisions[i] = 'approve'),
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text('Từ chối'),
selected: decisions[i] == 'reject',
onSelected: (_) => setState(() => decisions[i] = 'reject'),
),
]),
],
);
}).toList(),
),
actions: [
ElevatedButton(
onPressed: () {
sendApproval({
'type': 'approval',
'session_id': sessionId,
'approval_key': approvalBlock['approval_key'],
'decisions': decisions.map((t) => {'type': t}).toList(),
});
Navigator.of(context).pop();
},
child: const Text('Xác nhận'),
),
],
),
),
);
}