Agentaily —— 架构
🔁 架构收敛重写中(2026-06-19)。 本文件以下大部分描述的是旧模型(每垂类「zod 内容 schema」+ LLM 生成 renderer)。当前真相以 REFACTOR.md 为准 —— 已收敛到 两件套:
aml(后端数据唯一真相源 → 直接生成表 DDL + 自描述「GraphQL 精神」数据接口)+index.html(内容/样式都写在里面、LLM 可改、调数据接口);砍掉单独的 zod 内容 schema;StudiopostMessage协议保留;推进走 worktree + PR。 已落地(本轮):@agentaily/aml接入 workspace + emitter(AML AST → 表 DDL + API 描述符,56 测试);verticals/form与verticals/slides各补aml/(后端模型)+index.html(静态渲染页)。待办:运行时把 aml→真表+数据接口接进@agentaily/db/apps/web(rewrites 现有派生数据层,需谨慎/老板拍);版本自动快照 + 迁移 LLM(落地待定)。本文档待架构落定后整体重写。
「聊天 × 万物」(Chat × Everything):一个聊天 Agent,经按垂直划分的内容模型(每个 = 一份 JSON document + 一个 zod schema)构建制品(「万物」),每个都经一个可嵌入的 Studio 渲染与驱动。
本文档是整个平台的承重参考。加包、加垂直、或加 agent 能力前先读它。它经过两轮独立的架构评审(接缝 / 可扩展性 + 粒度 / 命名)审定 —— 下面的决策都是刻意为之。
唯一的核心想法
Chat × Everything = Agent ⟷ Studio Protocol ⟷ Studio.
- 一个 Agent 是通用的、领域无关的驱动者(chat · loop · tools · skills)。它不知道「form」是什么。
- 一个 Studio 是一个「物」(一个垂直):它持有一份作为其唯一真相源的 JSON document,渲染它,并暴露操作。
- Studio Protocol 是两者之间的契约。Agent 经它驱动任意 Studio。
加一个新「物」(slides、diagrams、dashboards…)= 定义一个新的内容模型(一份 JSON document + 一个 zod schema)+ 一个说同一套协议的 Studio。Agent、它的工具、技能、聊天 UI 都白送。「万物」就是这样 scale 的。
三层 + 产品
Tier 1 —— Agent(通用)
loop · tools · skills · providers (BYOK Anthropic, browser-direct) · chat UI。领域无关;从不 import 任何垂直。它只经协议驱动任意 Studio,别无其它。
Tier 2 —— Studio Protocol(接缝)
冻结的线缆契约(见下)。最需要保持干净的那一样东西。
Tier 3 —— Studios(万物)
每个垂直 = 一个内容模型(一份 JSON document + 一个 zod schema,该 schema 校验它、经 z.infer 给它类型、经 z.toJSONSchema 派生一个 JSON Schema)+ 一个实现该协议的可嵌入 Studio。Forms 优先。
产品
一个产品 = Agent(左)⊕ 一个 Studio(右),在 apps/ 里组合。例如 Form Design。
Studio Protocol(冻结的契约)
定义在 @agentaily/studio-protocol。这个包从不 import 任何垂直。
唯一真相源。 一份 JSON document → 两个投影:渲染出的 UI 与数据。没有第二个 schema 要保持同步。
信封是冻结的。 request / response / event 携带 source · id · method · params · result (opaque) · error · version · baseVersion。一切领域特定的东西都搭 invoke / describe,绝不改这个信封 —— 这也正是让未来的 MCP 适配器成为 1:1 映射的原因。
通用操作基座(每个 Studio 都支持这些):
| op | 含义 |
|---|---|
setDocument | 替换整份 document;返回 { ok, diagnostics, version } |
applyEdit | 应用一次局部编辑(patch);返回同样的 MutationResult |
getDocument | 当前 document(JSON 文本) |
getState | 结构化的当前状态 —— agent 的首要感知面 |
validate | 当前 document 的 StudioDiagnostic[] |
getSchema | 派生的数据 schema(形状在本层不透明;由产品收窄) |
getSchemaDiff | 从给定 schema 版本到当前的 SchemaDiff —— 给 LLM 驱动的升级用 |
screenshot | PNG data URL —— 仅用于视觉判断 / 给人看 |
describe | 能力自述(StudioCapabilities) |
invoke | 命名空间化领域 op 的逃生舱,例如 invoke('form.fieldCount') |
setRenderer | 设置 renderer —— 一份完整自包含的 index.html 字符串(或 null = 空白) |
getRenderer | 当前 renderer(index.html 字符串,或 null) |
领域操作绝不扩展 method 联合 —— 它们搭 invoke('<vertical>.<op>', params),并由 describe() 公布。日后映射到 MCP:invoke ≅ tools/call,describe ≅ tools/list。
Diagnostics 是垂直无关的,活在协议里: StudioDiagnostic { code, message, hint?, line?, col?, path? }。hint 是 agent 的修复信号 —— 把它当作一等输出。
版本 / 并发。 变更类 op 返回一个 version;host 传 baseVersion,陈旧的编辑会被拒(乐观并发)。防止人和 agent 同时碰文档时的盲目覆盖。
传输。 今天是 postMessage(页内 agent ⊕ 嵌入的 Studio;跨源安全 —— Studio 给自己的 DOM 截图并经 postMessage 回复)。同一个信封日后映射到 MCP,供外部 agent 用 —— 那个适配器是 MCP 唯一出现的地方。
工具 & 技能 —— 谁拥有什么
- Agent 拥有一套固定、通用的工具集,1:1 包装协议(
studio_set_document、studio_validate、studio_get_state、studio_set_renderer、studio_screenshot、studio_invoke)。 它绝不按垂直增长。 - 一个垂直拥有它自己的工具和技能(它面向 agent 的词汇),作为一个
StudioManifestship,由 Studio 经describe()公布。 - 产品(Agent ⊕ Studio)在启动时把那个 manifest 合并进 agent 的工具 / 技能列表。
- 默认走「编辑 document + validate」;只为被验证的热路径提拔一个定制工具。
所以词汇从垂直向上流动;它绝不被烤进 agent 里。这正是让 Tier 1 保持通用的原因。
感知优先级 —— agent 怎么「看」
严格顺序。agent 在结构、而非像素上推理:
getState/getSchema/getDocument—— 结构化、精确、廉价、确定性。对什么存在、什么有效的首要感知。validate()diagnostics —— 一次编辑后的首要反馈(带hint)。在这些上闭环,而不是在一张图片上。screenshot()—— 仅用于视觉 / 布局判断和给人看。绝不从截图里读字段或校验状态。
渲染层 —— 一个 schema 接口,一个 LLM 生成的 index.html renderer
一个 Studio 的「渲染侧」有两个关切,但只有第一个是平台 ship 的代码;第二个是 LLM 生成的数据(在聊天里编写,或由市场 preset 提供)。没有内置 renderer —— 一个全新项目是空白的,直到 LLM 生成一个。
概念:
- Vertical(垂直) —— 一类制品(form、slides)。定义 ① Schema 接口(一个 zod schema)。
- Document —— 一个内容实例:一份由垂直的 schema 校验的 JSON document。唯一真相源。
- ① Schema 接口(唯一发布的接口) —— 完整、renderer 无关的内容 模型(一个 form 的
fields[],一个 deck 的slides[]),作为一个 zod schema 定义一次,它校验 doc、 给它类型(z.infer),并为 LLM / renderer emit 一个 JSON Schema(z.toJSONSchema)。这是一个垂直为渲染侧 ship 的 唯一代码 —— 没有 renderer,没有 CSS。 - Renderer —— 一份完整、自包含的
index.html(结构 + 内联 CSS + 内联 JS;它 可引用 CDN 库/字体),针对 ① 构建。LLM 编写的数据,跑在 Studio 内部的一个内层 iframe 里。 外观活在同一个文件里 —— 没有单独的 theme。 无内置默认。
doc 如何到达 renderer(唯一真相源得以保持)。 renderer 是一个模板:它 包含一个空的 <script id="agentaily-doc" type="application/json"></script>。Studio 把 当前 document JSON 注入到那里,并在每次变化时(重新)加载 iframe;renderer 在加载时读取它 (JSON.parse(...))并渲染。内容与 renderer 保持为分离的制品 —— 同一个 renderer 适用于该垂直的任意 document。
拱心石规则: renderer 只针对 schema 接口构建,绝不针对 垂直的内部 —— 那个依赖严格单向。两个关切 —— 内容(JSON document) 和 renderer(index.html)—— 都可由 LLM 在聊天里编辑(studio_set_document / studio_set_renderer)。renderer 是数据,不是一个包:@agentaily/studio-protocol 把它 当作一个不透明的 HTML 字符串携带,@agentaily/presets 存储已发布的 renderer,Studio iframe 运行它们。
每个垂直一个市场。 一个 preset 是一个 renderer —— @agentaily/presets: 绑定到一个垂直的 { indexHtml, contract }。既然外观活在 index.html 里,就没有单独的 theme、也没有单独的 theme 市场 —— 每个垂直一个市场,每条目一份自包含 renderer。 市场不 ship 内置条目;它随用户发布而填充。你经聊天从零构建(一个空白 项目),或应用一个已发布的 preset 作基底、让 LLM 精修,然后把当前 renderer 作为一个 preset 发布。
组装: artifact = document (content, the single source of truth) × renderer (LLM-generated, or a preset)。document 保持纯内容;呈现是生成的。
状态: 已建。Studio 在一个内层 iframe 里托管 renderer(setRenderer 配一份完整 index.html,或 null = 空白),运行时注入 document;由 LLM 编写它。安全注记: renderer iframe 今天是同源(于是 CDN 库/字体 + html2canvas 截图能工作、且它有完整的网页能力)—— 对用户自己的项目 没问题;在 ship 跨用户共享的 preset 之前,把它隔离进一个独立的沙箱源。
这施加的要求: Schema 接口必须完整到足以据其渲染(不是一份 有损的摘要)—— form 和 slides 两个 schema 都是。
Schema 版本化 & LLM 驱动的迁移
内容模型的 schema 会演进;renderer 和 document 是针对某个特定形状构建的,所以 当 schema 变化时它们会落后。平台让演进可观察,并让 agent 把制品保持同步 —— 我们 ship 接缝,LLM 决定并应用。
- 版本化 schema(垂直级)。 每个垂直的 schema 包导出一个单调递增的
SCHEMA_VERSION+ 一个SCHEMA_CHANGELOG(作者手写:每次变更一条SchemaChange,打上 它落地的版本号,当旧 document 不再校验通过时标breaking,并带一个migrationHint)。describe()/getState()报告currentSchemaVersion。 - 陈旧检测。 制品记录它们是为什么构建的:一个 renderer/preset 携带
schemaVersion,一个项目持久化它内容的版本。Studio 给这些打戳 —— host 在加载一个旧制品时 传入保存的版本;一个 agent 写的制品默认为当前。于是getState暴露rendererSchemaVersion/docSchemaVersion;任一< currentSchemaVersion⇒ 陈旧。 - diff(一个来源,两个读者)。
getSchemaDiff(from)返回自from以来的SchemaChange[](纯 helperschemaDiff(changelog, from, to))。人把它看作一个 banner(「schema 升级 v1→v2:…」);agent 读同一份结构化列表来迁移。 - LLM 驱动的升级(可选,不强制)。 agent 工具
studio_schema_diff(fromVersion)暴露 change-list;系统提示告诉 agent:若一个制品陈旧,它可以升级 —— 经studio_set_renderer重写 renderer,且若有任何变更是 breaking,经studio_set_document迁移 document (受约束解码强制迁移后的 doc 符合新 schema)。Studio 随后把两者重新打戳到当前。无新增变更类 op —— 升级复用 set_renderer/set_document。
Monorepo 布局
边界是一个 monorepo 内的 workspace 包 —— 单一仓库装下一切。我们折叠进来,而不是拆分出去。
agentaily/ # 一个 monorepo(pnpm workspaces + Turbo)—— 承载一切的单一仓库
├─ llm/ @agentaily/llm # 多 provider LLM 网关(DeepSeek / 百炼·Qwen / 火山·Doubao / Anthropic + mock)—— agent 的模型层
├─ design-system/ @agentaily/design-system # React 组件库(120 组件 + Storybook);从自有仓库折叠进来 —— npm 发布已暂停,monorepo 为真相源
├─ presets/ @agentaily/presets # preset 注册表(renderer 市场 —— 每条目一份自包含 index.html,每垂直一个市场;无内置)
├─ eval/ @agentaily/eval # 任务完成度评测(真 agent + 真模型,headless),以 EVAL_KEY 把关
├─ backend/ @agentaily/backend # 数据采集逻辑 + 抽象存储接缝 + 内存测试替身 —— 纯逻辑(无基建、无 Drizzle、无 D1)
├─ db/ @agentaily/db # 持久化:Drizzle schema(表 SoT)+ 生成的迁移 + D1 支撑的 store(实现 backend 的接缝)
├─ studio/ # 垂直无关的 Studio 框架
│ ├─ protocol/ @agentaily/studio-protocol # 契约(承重接缝)
│ └─ client/ @agentaily/studio-client # host 侧 SDK,驱动一个 Studio(+ 防竞态 ready())
├─ agent/ # Agent(Tier 1)
│ ├─ agent/ @agentaily/agent # 无头:loop / tools / skills / memory(import @agentaily/llm)
│ └─ ui/ @agentaily/agent-ui # (legacy)原生 chat 挂载;已被 apps/web React UI 取代
├─ verticals/
│ ├─ form/ @agentaily/form-schema + @agentaily/form-studio # 垂直 #1 —— 表单(JSON document + zod schema,无 DSL)
│ └─ slides/ @agentaily/slides-schema + @agentaily/slides-studio # 垂直 #2 —— 幻灯片(JSON document + zod schema;验证了协议无需改动)
└─ apps/
└─ web/ @agentaily/web # 组合出的产品:项目管理器 → 每项目聊天 ⊕ 一个 Studio,基于 @agentaily/design-system命名规则:studio-* = 垂直无关框架 · form-* = form 垂直 · agent-* = agent · design-system = 共享组件库。
边界是一个 monorepo 里的包,不是分开的仓库。 前进计划是单一仓库
- 大 monorepo;我们把东西折叠进来,而不是拆分出去(设计系统从自有 仓库折叠进来;旧的多仓舰队正在退役)。早先「当 X 时抽成自己的仓库」的触发条件 被反转:优先 workspace 包,只为真正的外部消费者才发布到 npm(今天:没有新的 —— DS 发布已暂停)。
加一个新垂直(操作手册)
verticals/<x>/schema—— 内容模型:一份 JSON document + 一个 zod schema(像form/schema或slides/schema)。LLM 编写一份 JSON document;唯一一个 zod schema 校验它、给它类型 (z.infer),并为 LLM / renderer emit 一个机器可读的 JSON Schema(z.toJSONSchema)。 无手写文法 —— 两个垂直都是 JSON 原生的。verticals/<x>/studio—— 一个实现 Studio Protocol 的可嵌入 Studio:公布describe()(vertical、ops、schemaKind、manifest),并经invoke暴露领域 op。- Agent、
studio-client和聊天 UI 不变。在apps/里组合一个产品。
经验法则:如果一个玩具垂直需要改协议信封,那协议就没做完 —— 修协议,而不是修垂直。
状态
- 垂直 #1(forms) 和 #2(slides) —— 已在浏览器里端到端、跨源地构建并验证:
setDocument / applyEdit / getDocument / getState / validate / getSchema / screenshot / describe / invoke外加基于 version 的乐观并发。两者都是 JSON 原生的(一份 JSON document + 一个 zod schema)。 - Agent(Tier 1) —— 已建:loop · 通用的协议包装工具 · skill loader · memory (
@agentaily/agent),配一个最小的聊天挂载(@agentaily/agent-ui);在apps/web(产品壳)里 与 studio 组合。
精确契约(协议 op、数据形状、op 语义、#agentaily-doc 注入契约、schema 版本化)见 SPEC.md,接下来做什么见 ROADMAP.md,form document 的 zod schema(唯一真相源)见 verticals/form/schema/。