Skip to content

Agentaily —— 架构

🔁 架构收敛重写中(2026-06-19)。 本文件以下大部分描述的是旧模型(每垂类「zod 内容 schema」+ LLM 生成 renderer)。当前真相以 REFACTOR.md 为准 —— 已收敛到 两件套:aml(后端数据唯一真相源 → 直接生成表 DDL + 自描述「GraphQL 精神」数据接口)+ index.html(内容/样式都写在里面、LLM 可改、调数据接口);砍掉单独的 zod 内容 schema;Studio postMessage 协议保留;推进走 worktree + PR已落地(本轮):@agentaily/aml 接入 workspace + emitter(AML AST → 表 DDL + API 描述符,56 测试);verticals/formverticals/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 驱动的升级用
screenshotPNG 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_documentstudio_validatestudio_get_statestudio_set_rendererstudio_screenshotstudio_invoke)。 它绝不按垂直增长。
  • 一个垂直拥有它自己的工具和技能(它面向 agent 的词汇),作为一个 StudioManifest ship,由 Studio 经 describe() 公布。
  • 产品(Agent ⊕ Studio)在启动时把那个 manifest 合并进 agent 的工具 / 技能列表。
  • 默认走「编辑 document + validate」;只为被验证的热路径提拔一个定制工具。

所以词汇从垂直向上流动;它绝不被烤进 agent 里。这正是让 Tier 1 保持通用的原因。

感知优先级 —— agent 怎么「看」

严格顺序。agent 在结构、而非像素上推理:

  1. getState / getSchema / getDocument —— 结构化、精确、廉价、确定性。对什么存在、什么有效的首要感知。
  2. validate() diagnostics —— 一次编辑后的首要反馈(带 hint)。在这些上闭环,而不是在一张图片上。
  3. screenshot() —— 用于视觉 / 布局判断和给人看。绝不从截图里读字段或校验状态。

渲染层 —— 一个 schema 接口,一个 LLM 生成的 index.html renderer

一个 Studio 的「渲染侧」有两个关切,但只有第一个是平台 ship 的代码;第二个是 LLM 生成的数据(在聊天里编写,或由市场 preset 提供)。没有内置 renderer —— 一个全新项目是空白的,直到 LLM 生成一个。

概念:

  1. Vertical(垂直) —— 一类制品(form、slides)。定义 ① Schema 接口(一个 zod schema)。
  2. Document —— 一个内容实例:一份由垂直的 schema 校验的 JSON document。唯一真相源。
  3. ① Schema 接口(唯一发布的接口) —— 完整、renderer 无关的内容 模型(一个 form 的 fields[],一个 deck 的 slides[]),作为一个 zod schema 定义一次,它校验 doc、 给它类型(z.infer),并为 LLM / renderer emit 一个 JSON Schema(z.toJSONSchema)。这是一个垂直为渲染侧 ship 的 唯一代码 —— 没有 renderer,没有 CSS。
  4. 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[](纯 helper schemaDiff(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 发布已暂停)。

加一个新垂直(操作手册)

  1. verticals/<x>/schema —— 内容模型:一份 JSON document + 一个 zod schema(像 form/schemaslides/schema)。LLM 编写一份 JSON document;唯一一个 zod schema 校验它、给它类型 (z.infer),并为 LLM / renderer emit 一个机器可读的 JSON Schema(z.toJSONSchema)。 无手写文法 —— 两个垂直都是 JSON 原生的。
  2. verticals/<x>/studio —— 一个实现 Studio Protocol 的可嵌入 Studio:公布 describe()(vertical、ops、schemaKind、manifest),并经 invoke 暴露领域 op。
  3. 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/