人机协同的代理编辑
产品赌注是”代理构建,人类调试”。全自动变更适合草稿;生产环境需要信任边界。提议模式(ADR 0007)在现有 MCP 表面上提供这一边界 —— 没有第二套协议。
开启
const disconnect = await editor.connectMCP('ws://localhost:6410/ws?token=…', { mode: 'propose', // 默认 'auto' —— 变更立即应用 clientId: 'support-agent',})const queue = editor.mcpProposals! // propose 会话连接后非空信任边界,直说: 边界只约束选择加入的会话。同一编辑器上的 auto 会话仍可自由变更 ——
需要硬墙就让 propose 模式成为唯一连接。
代理看到什么
变更类调用不再应用。每个调用返回诚实的回执:
{ "proposed": true, "proposalId": 7, "provisionalNodeId": "01a0…", "queued": 3 }……代理提示词可以据此向模型解释队列的存在。铸造节点的工具(add_node、
instantiate_recipe)发放临时 id,代理可在后续提议中引用(connect_pins、
set_widget_value 等)—— 批准时翻译成真实 id。读取/查看类工具(get_graph、
find_nodes 等)仍对批准前的图 LIVE 执行。
审阅队列
queue.entries() // [{ id, ts, clientId, tool, summary, args }, …] 先进先出queue.onChange(size => renderBadge(size))
queue.approve() // 一个 command bus 事务:一个撤销步,原子queue.reject() // 全部丢弃(或传入 id)语义要点(ADR 0007):
- 原子批次。 approve 按序在一个事务内重放队列。任何失败都会回滚整个批次 —— 队列 保留以便重试。
- 批准时重新解析。 提议按”当前”图重放:引脚 label 与布局在批准时刻解析,而非提议 时刻。过期的提议会安全失败。
- 撤销按事务。 一次 Ctrl+Z 撤销最近一个已批准批次(或最近一次人工操作)—— 代理与人 的编辑通过串行化总线合法交错。没有 CRDT 魔法;并发编辑的合并语义明确不在范围内。
- 审计落在批准时。 提议不改变任何东西、也不写审计;批次应用时 audit 环记录它,带
效果增量与提议者的
clientId(audit log)。
内置审阅面板
你不必自己写 UI:从第一次 propose 连接起,编辑器会在有等待中的提议时显示角标,
点击即打开内置审阅面板 —— 每个待审操作显示工具、参数摘要、预期效果与客户端身份
(标注为 transport-provided、未认证),并提供 Approve all / Reject all 与单条
丢弃。Approve all 走 queue.approve() —— 整个批次是一个原子 undo 步。
editor.chrome.showProposals() // 打开面板(从未有过 propose 会话时返回 false)editor.chrome.hideProposals()editor.chrome.isProposalsVisible // boolean声明式包装组件 —— 挂载即打开,卸载即隐藏(自身不渲染任何 DOM):
<XenolithGraph …> <XenolithProposalQueue /> {/* 来自 @xenolithengine/graph-react */}</XenolithGraph>想要完全自定义审阅 UI 的宿主只需不打开内置面板,直接使用 editor.mcpProposals(见下)。
最小自定义审阅 UI
function AgentInbox() { const editor = useEditor() const [size, setSize] = useState(0) useEffect(() => editor?.mcpProposals?.onChange(setSize) ?? undefined, [editor]) if (!size) return null return ( <XenolithPanel position="top-right"> <b>{size} 条代理提议</b> <XenolithButton onClick={() => editor!.mcpProposals!.approve()}>全部批准</XenolithButton> <XenolithButton onClick={() => editor!.mcpProposals!.reject()}>拒绝</XenolithButton> </XenolithPanel> )}放在 <XenolithGraph> 内渲染;队列 API 是同步的,普通 state + onChange 钩子就够了。