@agentaily/aml
Agentaily Modeling Language(AML) —— 我们自家的声明式数据建模 DSL。自包含、 零依赖。Schema 就是纯 .aml 文本:model / enum / datasource / generator 块。
表层语法参考自 Prisma 的 schema 语言(一种熟悉、紧凑、易解析的形态)—— 但 AML 是我们自家的语言,不是 Prisma、也不绑定它。
它做四件事:
| 函数 | 是什么 | 为什么 |
|---|---|---|
parse(src) | 手写 lexer + 递归下降 parser → AST | 语法 |
validate(ast) | 语义检查 → 带行 / 列的 diagnostics | 语义 |
print(ast) | 规范化、幂等的格式化器(可 round-trip) | 输出 |
emit(ast) | AST → 物理表(DDL)+ 一个自描述的数据 API descriptor | 派生 |
它为什么存在: 给大模型一种声明式、基于文本的建模语言,它是被解析的、绝不 eval。模型吐出 AML 文本;我们把它解析成纯数据 AST,回递精确的错误 (line:col error: …),让它自修 —— 经典的 generate → check → fix 循环,且毫无执行 模型自撰代码的风险(那是你让它吐 zod/TS 会遇到的)。
内部包(
private: true),不发布。零运行时依赖。
API
import { parse, validate, print, check, format, formatDiagnostic } from '@agentaily/aml';
const src = `
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}`;
const { ast, diagnostics } = parse(src); // syntax only
const semantic = validate(ast); // names resolve, no dupes, relations point home
const all = check(src).diagnostics; // parse + validate in one pass
const pretty = format(src); // parse → canonical text (column-aligned)
for (const d of all) console.log(formatDiagnostic(d)); // "5:3 error: Unknown type 'Pst' …"parse是尽力而为(best-effort):语法错误会被记成一条 diagnostic、parser 随后恢复,所以你仍能一次拿到一个(部分的)AST 外加每一个错误。check(src)→{ ast, diagnostics },把语法 + 语义的发现合在一起。format(src)→ 规范化文本;幂等(format(format(x)) === format(x))。
Emit —— AML → 表 + 一个自描述的数据 API
AML schema 是一个 vertical 后端数据的单一真相源。emit 从一个解析好的 AST 派生出 两样东西:物理表(于是 schema 能直接生成 DDL),以及一个类型化、可内省的 API descriptor(「GraphQL 精神」),运行时把它开成查询 / 写入端点 —— LLM 撰写的 index.html 则 READ 它来决定调哪些端点。
import { check, emit, emitTables, emitApiDescriptor } from '@agentaily/aml';
const { ast } = check(src); // emit assumes a validated AST (throws on an unknown type)
const { tables, sql, api } = emit(ast); // one shot; or call the two halves on their own:
emitTables(ast); // → { tables: TableDef[], sql } physical structure + DDL
emitApiDescriptor(ast); // → { entities, enums } the introspectable contractemitTables(ast)→{ tables, sql }—— 每个model一个TableDef(name、model、columns、primaryKey、uniques、indexes,以及它自己的CREATE TABLEsql)。结果层级的sql是完整 DDL:每个CREATE TABLE IF NOT EXISTS加每个CREATE INDEX,用 SQLite / D1 方言。emitApiDescriptor(ast)→{ entities, enums }—— 每个 model 一个EntityDescriptor(逻辑字段名、类型、required/unique/isId、背后的column、 enum 值、关系目标),于是客户端能内省「有哪些 entity/字段、我怎么读 / 写它们」。
类型映射(SQLite/D1): String → TEXT · Int/Boolean → INTEGER(bool 存为 0/1)· Float → REAL · DateTime → INTEGER(epoch ms;descriptor 标 format: 'epoch-ms')· Json → TEXT(form 的「答卷」全答案列)· Type[] → TEXT (JSON 数组)· enum 引用 → TEXT(descriptor 带上允许的值)。Type? → 可空; @id/@@id → PRIMARY KEY;@unique/@@unique → UNIQUE;@@index → CREATE INDEX; @map/@@map → 物理列 / 表名。
什么是、什么不是列: 类型为标量或 enum 的字段成为一个物理列;类型为另一个 model 的字段是一个关系(导航)—— 它没有列(只有那个标量 FK 字段才有),但它仍以 可导航关系的身份出现在 API descriptor 里。系统列不被强加:AML model 声明它们自己的 键,所以列恰好就是被建模的那些字段。
默认值: 字面量 @default(...)(string/number/bool/enum)→ 一个 SQL DEFAULT; Int @id 上的 autoincrement() → INTEGER PRIMARY KEY AUTOINCREMENT; now()/cuid()/uuid() 会在 TableDef/descriptor 上被结构化地捕获,但不吐出 SQL DEFAULT(它们由运行时 / app 填)。
支持的子集
块: datasource、generator、model、enum。
标量类型: String、Int、Float、Boolean、DateTime、Json。任何其它类型名 都必须解析到一个 model 或 enum,否则就是一个 Unknown type 错误。
字段修饰符: 可选 Type?、列表 Type[]。
字段属性: @id、@unique、@default(…)、@map(…)、@updatedAt、@relation(…) (含命名参数,如 @relation(fields: [authorId], references: [id]))。文法接受任意 @name(args…),所以未知属性也能解析 —— 由 validate 来裁决它们。
块属性: @@id、@@unique、@@index、@@map。
值: 字符串、数字、布尔、标识符 / enum 引用、函数调用(now()、autoincrement()、 env("…")、cuid()),以及数组([a, b])。
注释: /// 文档注释附着到下一个声明 / 字段上(以 documentation 留在 AST 上); // 注释被丢弃。
validate 检查什么
- 顶层名重复(model + enum 共用一个命名空间)。
- 一个 model 内字段重复;一个 enum 内值重复。
- 字段类型解析到一个标量、model 或 enum。
@relation(fields: […])列的字段在该 model 上存在。@@id/@@unique/@@index引用的字段存在。- 警告: 一个有字段但没有
@id/@@id主键的 model。
刻意不在范围内(「medium」子集)
原生类型映射(@db.VarChar、…)、BigInt / Decimal / Bytes、复合 type 块、 多 schema / 多 datasource 的边角情况。这些按设计会报 Unknown type / 意外语法的错 —— 这里的 LLM 驱动数据建模不需要它们。
AST 形状
parse 返回 { ast: SchemaAst, diagnostics: Diagnostic[] }:
interface SchemaAst {
datasources: DatasourceDecl[]; // { name, assignments: { key, value }[] }
generators: GeneratorDecl[]; // same shape as datasource
models: ModelDecl[]; // { name, fields, attributes(@@…), documentation? }
enums: EnumDecl[]; // { name, values, documentation? }
}每个节点都带一个 Span({ start, end },含 0 起始的 offset + 1 起始的 line/column),所以 diagnostics —— 以及你自己的工具 —— 都能指向精确的源位置。 完整集合见 src/types.ts。
开发
pnpm --filter @agentaily/aml typecheck # or: pnpm exec tsc --noEmit -p aml/tsconfig.json
pnpm --filter @agentaily/aml test # or: pnpm exec vitest run --root aml已在 monorepo workspace 里注册(
@agentaily/aml),并被根 vitest run 收录。