ΓDocsInteractiveNet

05 · Ainet 语言设计

目标:给两类人同一套语义—— 研究者/系统工程师用 Core 精确描述网与规则; Agent 工程师用 Flow 写流水线、扇出、竞速、监督,不被迫先学 λ 演算。 两者编译到同一个 AIN IR。

5.1 设计目标

目标 具体要求
可读 网/端口/规则是显式的;不要求用户手写组合子
可验证 线性端口、session 协议、effect row、预算都能静态检查
可移植 编译到 aind 字节码、WASM(Netbook)、以及可选的 HVM/IC 后端
可调试 每个源位置映射到 cell/rule;错误能定位到图
可扩展 宿主函数/工具/模型通过 effect 接口接入,不改核心
渐进 先写 Flow;需要时下探到 Core;再需要时写裸 IR

5.2 两层语言

Layer 1:Ainet Core(显式网)

核心概念直接暴露:agent、port、state、rule、net、effect。

// 类型
type Task   = { id: Text, prompt: Text }
type Draft  = { text: Text, cites: List<Text> }
type Report = { text: Text, approved: Bool }

// 效果(由宿主提供实现)
effect LLM {
  input  { model: Text, prompt: Text, max_tokens: Nat }
  output { text: Text, tokens: Nat, usd: Float }
}

effect Search {
  input  { query: Text, k: Nat }
  output List<{ title: Text, url: Text, snippet: Text }>
}

// Agent:一个带状态与端口的 cell
agent Researcher {
  ports {
    inbox: ?Message          // principal:接收消息
    out:   !Draft            // auxiliary:发送结果
    llm:   !LLM              // auxiliary:能力(capability)
    search: !Search
  }
  state Idle | Searching { prompt: Text } | Thinking { docs: List<Doc> } | Done
}

// 收到任务 → 先搜索;回复回到 inbox(复用同一个端口)
rule Researcher[Idle](inbox, out, llm, search) <> Task(t) =>
  Researcher[Searching(prompt: t.prompt)](inbox, out, llm, search)
  | search.call(query: t.prompt, k: 5, reply: inbox)

// 搜索完成 → 再调用 LLM
rule Researcher[Searching { prompt }](inbox, out, llm, search) <> Search.Response(docs) =>
  Researcher[Thinking(docs: docs)](inbox, out, llm, search)
  | llm.call(
      model: "fast",
      prompt: render(prompt, docs),
      reply: inbox
    )

// LLM 完成 → 输出草稿
rule Researcher[Thinking { docs }](inbox, out, llm, search) <> LLM.Response(r) =>
  Researcher[Done](inbox, out, llm, search)
  | Send(out, Draft{ text: r.text, cites: urls(docs) })

注意:每个状态只接受一种回复。如果同时发出 search 与 llm 两个调用,两条回复会竞争同一个 inbox,其中一个会因为没有匹配规则而卡住。这是故意的约束:Agent 的状态应当明确它此刻在等什么;需要并发等待时,用 Join(2) 而不是复用同一个 principal 端口。

构造子即 cell:Task(t)、Search.Response(docs)、Draft{...} 不是"结构体字面量",而是携带字段的构造子 cell(agent)。t.prompt 是规则对消息字段的投影。这样数据流和消息流是同一种图结构。

能力与 Effect 的关系:llm 是一个 capability cell(内部由 Effect<LLM> 实现),llm.call(...) 展开为 EffectCall(llm, op, input, reply) + 一条 reply wire;运行时把 reply 注入到 reply 所在的端口。

要点:

  • inbox 是 principal 端口;其余是 auxiliary。
  • llm.call(..., reply: inbox) 是一个 EffectCall cell:principal 连到 llm 能力,aux 连到 payload 与 reply wire。回复被注入到 reply 所连的端口(这里是新 agent 的 inbox),形成新的活跃对。
  • 规则 RHS 是网表达式:cell | cell | ...;Send(port, value) 把一个值送到辅助端口。
  • 状态展开由编译器完成:Researcher[Idle] / Researcher[Working] / Researcher[Done] 是不同 agent 符号。

Layer 2:Ainet Flow(Agent DSL)

大多数用户不写规则,只写"有哪些 Agent、怎么连、预算多少":

flow Research {
  input  question: Text
  output report: Report
  budget { usd: 0.80, tokens: 250_000, wall: 120s }

  agent R = Researcher(model: "fast")
  agent C = Critic(model: "strong")
  agent W = Writer(model: "fast")

  stage retrieve = R.search(question, k: 5)
  stage draft    = R.draft(question, retrieve)
  stage review   = C.review(draft)
  stage write    = W.write(draft, review)

  draft -> review -> write -> report
}

(retrieve 作为 draft 的参数传入;箭头只表示输出到下一 stage,不重复连接。)

Flow 的编译结果是一张网:

Question ─▶ Researcher ─draft─▶ Critic ─verdict─▶ Writer ─▶ Report
                 │                 │                 │
                 └── LLM cap ──────┴── LLM cap ─────┘

组合子

// 扇出 + join
par {
  branch a = R.draft(question)
  branch b = R.draft(question)
} -> join { a, b } as both

// 竞速(第一个成功)
race {
  M1.answer(question)
  M2.answer(question)
} -> first

// 超时
with timeout(30s) {
  R.draft(question)
}

// 监督
supervise C {
  policy: restart_one
  max_restarts: 3
  window: 60s
}

下探

Flow 的每个 stage 都可以标注 core: 覆盖生成的规则;Core 的每个 agent 也可以在 Flow 里当 stage 用。二者不是两门语言,而是同一 AST 的两种表面语法。

5.3 语法(Core,EBNF 摘要)

program     ::= decl*
decl        ::= typeDecl | effectDecl | agentDecl | ruleDecl | flowDecl | importDecl

typeDecl    ::= "type" ID "=" type ";"
type        ::= ID | "{" field,* "}" | "List" "<" type ">" | "Result" "<" type "," type ">"
              | "(" type,* ")" | type "->" type

effectDecl  ::= "effect" ID "{" "input" type "output" type "}"

agentDecl   ::= "agent" ID "{" portDecl* stateDecl? "}"
portDecl    ::= ID ":" ("!" | "?") type ";"
stateDecl   ::= "state" variant ("|" variant)*
variant     ::= ID ("{" field,* "}")?

ruleDecl    ::= "rule" pattern "<>" pattern "=>" net ";"
pattern     ::= ID ("[" statePat "]")? "(" ID,* ")"
statePat    ::= ID | "_"
net         ::= cell ("|" cell)*
cell        ::= ID ("[" statePat "]")? "(" arg,* ")"
arg         ::= ID ":" expr | expr
expr        ::= literal | ID | expr "." ID | "render" "(" expr ")"

flowDecl    ::= "flow" ID "{" flowItem* "}"
flowItem    ::= "input" ID ":" type
              | "output" ID ":" type
              | "budget" "{" budgetField,* "}"
              | "agent" ID "=" ID "(" arg,* ")"
              | "stage" ID "=" call
              | portChain
call        ::= ID "." ID "(" arg,* ")"
portChain   ::= ID ("->" ID)+ ("->" ID)?

语法细节(运算符优先级、注释、字符串插值)在 specs/grammar.md 固化;本文只保证示例可读。

5.4 类型系统

5.4.1 端口类型

  • !T:发送 T(output/capability)。
  • ?T:接收 T(input)。
  • 端口是线性的:每个端口在一条规则里恰好被消费一次;free port 不能被复制。
  • 需要共享时,显式使用 dup(编译器插入 δ)或 Ref cell;类型系统会记录共享点。

5.4.2 Session 协议

type LLM = session {
  send Request;
  recv Response;
  end
}

session { ... } 是类型级糖,等价于 !Request.?Response.end;完整语法见 specs/grammar.md。v1 支持 send/recv/end/rec。

或内联:

!{ request: Request; response: Response; end }

v1 支持:

  • send T / recv T / end
  • rec X. ...(递归协议)
  • choice { ... }(内部选择)与 offer { ... }(外部选择)——可选
  • 对偶检查:连接的两端必须互补;否则类型错误。

5.4.3 Effect row

agent Researcher effects { llm, search } {
  ...
}
  • 规则只能调用声明过的 effect。
  • 运行时 handler 必须覆盖所有声明的 effect;缺失则在启动时报错。
  • 纯 agent(无 effect)可以编译到纯网后端(HVM/WASM),获得更高性能。

5.4.4 状态与数据

  • state 是有限 ADT;编译器展开为 agent 变体。
  • 数据类型是结构化 JSON-ish;大 payload 存堆/对象存储,cell 里只放引用。
  • Result<T,E> 是一等类型;Error 是它的构造子,不是异常。

5.4.5 预算类型

agent Researcher budget { usd: 0.20, tokens: 80_000 } {
  ...
}
  • 预算是上限,不是保证;运行时在 effect 前检查。
  • 子 agent 的预算从父预算扣减;超支返回 Denied。
  • 类型检查保证每个 effect 调用都有对应的预算来源(或显式 unbounded,仅限测试)。

5.5 编译流水线

source (.ain)
   │  parse (rowan / chumsky)
   ▼
AST ── resolve names ──▶ typed AST
   │  linear + session + effect + budget checks
   ▼
Core AST (agents/rules/nets)
   │  lower: state expansion, effect calls, join/select, supervision
   ▼
AIN IR (cells, ports, wires, rules; SSA-ish)
   │  optimize: inline, specialize, fuse, dead-cell elim, placement
   ▼
Bytecode / WASM / HVM-IC (optional)

关键 lowering

源构造 IR
agent Researcher[Idle] agent symbol Researcher_Idle
state 转移 规则 RHS 替换为新符号
llm.call(...) EffectCall cell + reply wire + budget check
par { ... } δ 复制输入 + 多个子网 + Join
race { ... } δ + 多个子网 + Select + cancel
supervise Supervisor cell + 子 Exit 端口
with timeout Timeout cell 包装子网
flow 的 -> 端口对接 + 消息 cell
stage 一个 agent 实例 + 输入/输出 wire

优化(v1 只做安全子集)

  • 内联:单次使用的 agent 可以内联其规则(减少 cell)。
  • 特化:状态已知时消去不可达分支。
  • 融合:相邻的 Send/Receive 合并。
  • 死 cell 消除:没有路径到 free port 的子网擦除(ε)。
  • 放置:按 @loc/租户/通信图分配。

5.6 示例:三 Agent 研究流水线(Flow)

flow Research {
  input  question: Text
  output report: Report
  budget { usd: 0.80, tokens: 250_000 }

  agent R = Researcher(model: "fast",  temperature: 0.3)
  agent C = Critic(model: "strong", temperature: 0.0)
  agent W = Writer(model: "fast",  temperature: 0.5)

  stage docs   = R.search(question, k: 8)
  stage draft  = R.draft(question, docs)
  stage review = C.review(draft)

  if review.approved {
    stage final = W.write(draft, review)
    final -> report
  } else {
    stage revised = R.revise(draft, review)
    stage review2 = C.review(revised)
    W.write(revised, review2) -> report
  }
}

编译后的网(简化):

Question
   │
   ▼
Researcher ──docs──▶ Researcher ──draft──▶ Critic ──review──┐
   │                    │                    │              │
   └── Search cap ──────┴── LLM cap ─────────┴── LLM cap ───┤
                                                            ▼
                                              ┌── approved? ──┐
                                              │ yes           │ no
                                              ▼               ▼
                                            Writer         Researcher.revise
                                              │               │
                                              └─────▶ Report ◀┘
  • if 在 IR 里是 Select/Switch cell;两个分支的网都被构造,未选分支被擦除(或取消)。
  • review.approved 是一个普通数据字段;分支选择由 Switch 规则完成。
  • 预算在 Researcher/Critic/Writer 之间分配;Search 与 LLM 调用前检查。

5.7 示例:扇出 + join + 竞速(Core)

// 扇出:把问题复制给两个模型
rule Fanout[Idle](in, outA, outB) <> Question(q) =>
  Fanout[Done](in, outA, outB)
  | Send(outA, Question(q))
  | Send(outB, Question(q))

// 竞速:第一个答案获胜,另一个取消
rule Race[Idle](in, a, b, out) <> Question(q) =>
  Race[Waiting](in, a, b, out)
  | Select2(a, b, out, cancel: in)

Select2 是 L2 的 Select(2) 语法糖;失败/取消的输入收到 Cancel 消息。上游 EffectCall/Agent 必须声明 cancel 端口,否则类型错误。

5.8 示例:监督 + 重试

supervise Worker {
  policy: restart_one
  max_restarts: 3
  window: 60s

  agent W = Worker(model: "fast")
  stage job = W.run(task)
  job -> result
}

编译为:

Supervisor ──spawn──▶ Worker
     ▲                  │
     └──── Exit(ok|err) ┘
  • restart_one:只重启失败的子 agent;保留其他子网。
  • max_restarts/window 由 Supervisor cell 的计数器状态实现。
  • 超过阈值 → Escalate 到父 supervisor;没有父则终止整个 flow 并返回 Error。

5.9 TypeScript SDK

面向不写 .ain 的开发者:

import { defineAgent, flow, llm, tool } from "@ainet/sdk";

const researcher = defineAgent({
  name: "Researcher",
  input: Task,
  output: Draft,
  capabilities: {
    llm: llm({ model: "fast", budget: { usd: 0.2 } }),
    search: tool("search", { schema: SearchInput }),
  },
  async run(task, ctx) {
    const docs = await ctx.search({ query: task.prompt, k: 5 });
    const draft = await ctx.llm.ask(render(task.prompt, docs));
    return { text: draft.text, cites: docs.map(d => d.url) };
  },
});

const research = flow("research")
  .input("question", Task)
  .output("report", Report)
  .stage("draft", researcher)
  .stage("review", critic)
  .stage("write", writer)
  .budget({ usd: 0.8, tokens: 250_000 });
  • SDK 在进程内运行 ain-runtime(napi/wasm),或连到 aind 服务。
  • defineAgent.run 是普通 async 函数;SDK 把它编译成状态机 cell(等价于 Core 的规则)。
  • 能力(capability)通过 ctx 注入,不是全局注册表;没有 ctx.search 就调不到。
  • flow 是 DSL builder;最终生成同一份 AIN IR。

5.10 错误信息与 DX

目标:错误要能"指到图上"。

示例:

error[E-LINEAR-01]: port `out` is used twice
  --> research.ain:24:18
   |
24 |   | Send(out, draft) | Send(out, review)
   |         ^^^            ^^^ `out` already consumed here
   |
help: duplicate the value explicitly:
   |   | dup(draft) as d1, d2
   |   | Send(out, d1) | Send(out, d2)

其他错误:

  • E-SESSION-01:协议对偶不匹配(发送 vs 发送)。
  • E-EFFECT-01:调用了未声明的 effect。
  • E-BUDGET-01:effect 没有预算来源。
  • E-DEADLOCK-01:Join/Select 的环形等待(保守检查)。
  • E-STATE-01:状态未覆盖(缺失规则)。
  • E-PORT-01:端口类型/方向不匹配。

每个错误带:源位置、相关 cell/port、最小复现网(可粘贴到 Netbook)、修复建议。

5.11 模块、包、版本

project/
├── ainet.toml
├── src/
│   ├── main.ain
│   ├── agents/
│   └── flows/
└── tests/

ainet.toml:

[package]
name = "research"
version = "0.1.0"
edition = "2026"

[dependencies]
ainet-std = "0.1"
  • 包 = 一组 agent/effect/flow;可发布到 registry(v2)。
  • 版本化:IR 有 version 字段;运行时拒绝不兼容的字节码。
  • 能力声明在包级别:安装包时能看到它请求的 effect(类似权限清单)。

5.12 与 AIN IR 的映射(摘要)

Ainet AIN IR
agent + state 多个 agent 符号
ports principal/aux 端口 + 类型
rule (α,β) -> template
Send(port, v) Message cell 接到 aux 端口
EffectCall Effect<Op> cell + job
par δ + Join
race δ + Select + Cancel
supervise Supervisor + Exit 端口
budget Budget cell + 准入检查
flow 网拓扑 + 初始消息

5.13 v1 实现范围

必须:

  • Core 的 agent/rule/net/effect/type。
  • Flow 的 agent/stage/->/par/race/timeout/supervise/budget。
  • 线性 + session(!/?/end/rec)+ effect row 检查。
  • 编译器到 aind 字节码 + WASM。
  • 错误信息带源位置与网片段。

可以延后:

  • 泛型/子类型/row 推断。
  • 完整 &/⊕ session 子类型。
  • 热更新/动态路由表。
  • 包 registry。
  • Python SDK(先 TS)。

在 GitHub 编辑本文