Xây dựng agentic-ui-vue: Để LLM vận hành ứng dụng Vue một cách an toàn
Chúng tôi muốn AI assistant thật sự vận hành ứng dụng Vue: điền form, tìm kiếm trong table, cập nhật Pinia store hoặc lưu một record. Hai hướng tiếp cận hiển nhiên đều nhanh chóng bộc lộ vấn đề.
DOM automation yêu cầu model diễn giải pixel rồi click button. Cách này dễ vỡ khi giao diện đổi thiết kế và bỏ qua ngữ nghĩa ứng dụng. Raw LLM function-calling tốt hơn, nhưng thường là một danh sách function phẳng ghép thủ công. Nó không biết form nào đang mount, action nào tạm bị vô hiệu hóa, hay user muốn panel nào trong hai panel giống nhau.
Câu trả lời của chúng tôi trong agentic-ui-vue là một app-native tool registry. Component khai báo các capability có type, description và risk tag. Model gọi chúng, nhưng chúng chạy bên trong ứng dụng, trên đúng state và Pinia store mà UI đang sử dụng.
const todoStore = useTodoStore()
useAgentCapability({
name: 'todos.add',
description: 'Add a new item to the current todo list.',
inputSchema: {
type: 'object',
properties: { text: { type: 'string' } },
required: ['text'],
additionalProperties: false,
},
risk: 'mutating',
execute: ({ text }: { text: string }) => todoStore.add(text),
})Pinia vẫn là app state; capability chỉ là contract tường minh quy định cách agent được phép tác động lên state đó.
Catalog đi cùng lifecycle của UI
Quyết định thiết kế đầu tiên là catalog phải có cùng lifecycle với giao diện. useAgentCapability register trong onMounted, cập nhật qua registration handle khi definition reactive thay đổi, suspend khi isAvailable chuyển thành false và dispose trong onUnmounted.
Nhờ vậy, tool list khớp với màn hình. Đóng drawer thì capability của nó biến mất. Nếu tool-call đã được gửi, bước resolve trả về unavailable cùng hint yêu cầu liệt kê lại tool. Kết quả quay lại agent loop để model tự sửa hướng đi.
Một safety pipeline, không có đường tắt
Mọi call đều đi vào qua registry.execute(). Chúng tôi chủ ý chỉ giữ một execution path:
resolve → stale-check → isAvailable → validate(inputSchema)
→ permission → confirmation → executeResolution chọn capability và target. Revision check chặn catalog cũ. Runtime availability đưa ra quyết định cuối. Schema validator kiểm tra input trước permission và confirmation. Chỉ sau đó application code mới được execute.
Mỗi capability khai báo một trong ba risk level: read, mutating hoặc destructive. read mặc định không cần confirmation; hai mức còn lại dùng confirmation gắn với digest. Permission hook chạy trước, vì không nên yêu cầu approve một operation mà user không có quyền thực hiện.
Approval phải thuộc về payload
Approve “save tool” là quá rộng. Câu hỏi hữu ích phải là user có approve lần save này, với arguments này, cho target này hay không.
Với risky call, core tính SHA-256 trên canonical JSON chứa { name, targetId, input }. Object key được sort, whitespace bị loại bỏ, tạo thành digest ổn định 64 ký tự. Đổi một byte của canonical payload làm digest đổi nên ứng dụng hỏi lại. Approval có thể được memoize cho digest hoặc session.
Vue package cung cấp một headless queue; host app sở hữu dialog thực tế:
const { registry } = useAgent()
const queue = useConfirmationQueue()
registry.setConfirmationHook(queue.hook)
// In the approval UI:
queue.pending.value[0]?.approve('digest')
// or: queue.pending.value[0]?.deny('Not now')Promise của queue giữ registry.execute() ở trạng thái block cho đến khi UI resolve nó. Không có modal đóng gói sẵn, không có security theater và không có approval âm thầm tiếp tục hiệu lực sau khi payload thay đổi.
Route đến đúng instance
Hai form có thể cùng expose form.save, nên nhúng component id vào capability name sẽ tạo catalog nhiễu và thiếu ổn định. Thay vào đó, mỗi registration khai báo một target:
useAgentCapability({
name: 'form.save',
target: { id: props.panelId, label: props.title },
description: 'Save the values in this form.',
inputSchema: { type: 'object', properties: {} },
risk: 'mutating',
execute: saveForm,
})Khi nhiều registration active có cùng name, bước serialize request chỉ phát ra một tool và inject _target enum vào schema. Registry loại _target trước khi validate schema thật của capability rồi route call đến instance được chọn. Nếu target đó đã biến mất, kết quả là unavailable và hint liệt kê các target còn sống. Model có thể chuyển hướng thay vì đoán.
Revision khiến thời gian trở nên hữu hình
Mount, replace, suspend, resume và dispose đều tăng catalog revision. Khi client serialize tool, nó snapshot revision và gắn vào call sau đó. Mỗi registry entry cũng lưu revision của breaking registration hoặc replacement gần nhất.
Call dựa trên definition cũ trả về stale thay vì execute. Đây là optimistic concurrency control cho góc nhìn của agent về UI, an toàn hơn giả định page đứng yên khi model suy luận.
Ba tool thay vì năm mươi
Gửi mọi schema phù hợp với ứng dụng nhỏ. Khi có hơn 50 capability, cả prompt size lẫn khả năng chọn tool đều tệ đi. Meta-tools mode chỉ gửi:
buildRequestTools(registry, { mode: 'meta-tools' })
// agent.search_capabilities
// agent.describe
// agent.executeModel search name và description, describe một capability theo kiểu lazy rồi execute nó. Điểm quan trọng là agent.execute gọi registry.execute() cho capability thật. Wrapper mang risk read; validation, permission và confirmation chạy lại ở inner call, còn digest chứa real tool name, target và input — không phải payload của meta-tool.
Dấu chấm đã làm vỡ agent loop thật
Failure giá trị nhất của chúng tôi đến khá muộn. Chúng tôi dùng dotted namespace như form.save và users.table.search; chúng dễ đọc và tự nhiên. Các test với MockLanguageModelV2 chấp nhận chúng, và toàn bộ loop trông hoàn toàn khỏe mạnh.
Sau đó chúng tôi chạy cùng loop với model thật qua local 9Router gateway. Request thất bại với mã 400. Các OpenAI-compatible provider yêu cầu function name khớp ^[a-zA-Z0-9_-]+$. Dấu chấm là ký tự không hợp lệ.
Cách sửa là xem provider naming như một wire concern. toWireName() sanitize catalog name tại LLM boundary — form.save trở thành form_save — còn bindRegistryToChat dựng lại wire-to-real map từ live catalog và khôi phục form.save trước execution. Collision detection ngăn hai real name âm thầm map vào cùng một wire name.
Live test đó tìm ra thêm một edge mang đặc trưng provider: Responses API mặc định của AI SDK truyền sai tool-call id qua gateway. Proxy của chúng tôi hiện dùng openai.chat(), tức Chat Completions path mà các OpenAI-compatible gateway expose.
Bài học sắc hơn câu “hãy thêm integration test”. Mock LLM rất tốt cho deterministic behavior, nhưng nó tái hiện contract mà chúng ta tưởng mình có. Model và provider thật tái hiện contract mà chúng ta thực sự có.
Boundary sạch, test tốt hơn, internals nhìn thấy được
Chúng tôi giữ @agentic-ui/core, @agentic-ui/schema và @agentic-ui/protocol hoàn toàn Vue-free; architecture test cùng ESLint rule enforce boundary đó. @agentic-ui/vue cung cấp lifecycle và reactivity. @agentic-ui/client sở hữu AI SDK transport và wire naming. @agentic-ui/testing có thể chạy toàn bộ registry pipeline như gọi một function thông thường:
const result = await callCapability(capability, input, {
permission: true,
confirm: 'approve',
})Như một phần bonus, @agentic-ui/vue tích hợp với @vue/devtools-api: live capability inspector hiển thị name, target, risk, state, schema và revision; audit timeline hiển thị các event received, validated, permitted, confirmed, executed hoặc rejected. Thế giới mà agent nhìn thấy không còn vô hình.
agentic-ui-vue được phát hành theo giấy phép MIT. Hãy bắt đầu với getting started guide, hoặc khám phá repository.