Aura 0.1.0:可以钉住的第一个版本,以及代码为什么要当纸带

Aura 0.1.0 在 2026-11-07 打了标签。这是第一条可以钉住的发布通道。默认安装仍然是 nightly,每晚跟着 main 换。要这一版,把版本指到 v0.1.0。

接口还没有冻结。这一版保证的是:在它支持的系统上,装得上,自带的例子跑得起来。

口号是:让代码第一次拥有思考的能力。这里的思考不是意识。它是一件很小的事。程序读到自己正在用的规则,试一条新的,合适就留下,不合适就撤回。纸带还是那一条。


这一版钉住什么

0.1.0 是安装通道,不是另一门语言。

1
2
curl -fsSL https://raw.githubusercontent.com/cybrid-systems/aura/main/install.sh | sh
AURA_VERSION=v0.1.0 curl -fsSL https://raw.githubusercontent.com/cybrid-systems/aura/main/install.sh | sh

第一行装的是滚动的 nightly。第二行才是 0.1.0,对应发布文件 aura-0.1.0-linux-<arch>.tar.gz。镜像是 ghcr.io/cybrid-systems/aura:0.1.0,另外有 :0.1 和 :latest。:nightly 继续指向 main。:latest 只由带版本号的标签更新,不会写进 aura new 生成的项目。版本号是 X.Y.Z 的二进制,aura new 会把项目钉在同一个通道上。dev 和 nightly 写 :nightly。

主机是 Linux,x86_64 和 arm64。安装和冒烟覆盖 Ubuntu 24.04、Ubuntu 26.10、Debian 12、Rocky Linux 9、openEuler 24.03 LTS SP4。macOS、Windows 和其他发行版不在这一版里。打标签之前会跑一场 300 秒的并发浸泡:8 个工人,256 条 fiber。

进化策略那套独立的 starter kit 还没有钉到这个标签上。


装上之后,例子在改什么

0.1.0 的入口是:

1
2
3
aura new evolve demo
cd demo
aura run

它先把一个定义装进正在跑的工作区:

1
2
(set-code "(define params (quote (0 0)))")
(eval-current)

然后每一步都是同一串动作。记住这一刻,试一个新的 params,门禁认了就留下,不认就把工作区放回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(define (evolve! steps)
(let loop ((i 1) (rows '()))
(if (> i steps)
rows
(let* ((cur (live-params))
(cand (propose cur))
(snap (ast:snapshot (format "step-~a" i)))
(ok (mutate:rebind "params" (params->src cand) "evolve")))
(eval-current)
(if (and (eq? ok #t) (keep? cur cand))
(loop (+ i 1) (cons (list i "KEEP" cand (fitness cand)) rows))
(begin
(ast:restore snap)
(eval-current)
(loop (+ i 1) (cons (list i "ROLLBACK" cand (fitness cand)) rows))))))))

目标是靠近 (3 -2)。候选是当前这对整数各自随机走 −1、0 或 1。keep? 只问新的那对是否更近。终端上能看到留下了几步、撤回了几步,以及最后的 params。同一次运行还写 out/report.html 和 out/audit.tsv。那是给人翻的记录。真值在工作区里:params 这个定义还是原来那个,引号里的数换过,不合适的换回去了。

aura new app 是普通的模块和测试。aura new agent 是一条编排好的流水线。三份模板里,能看见「规则被改写」的是 evolve。


计算仍是纸带上的改写

计算是一件有穷的事。给一串符号,按定好的规则,机械地写出另一串符号。

图灵机把这件事拆成三件:一条可以无限延长的纸带,一个读写头,一张状态转移表。头在纸带上移动,读一格,写一格,自己进入下一个状态。λ 演算用另一套词说同一件事。(λx. x+1) 作用到 2 上,归约成 3。函数的定义、应用和归约,就是那张转移表的另一种写法。丘奇–图灵论题说,一边能算的,另一边也能算。两边一起画出「可计算」的边界。

我们手里的计算机是一台有穷的图灵机。内存是纸带上截下来的一段。程序计数器和译码器是那个状态机。LOAD 和 STORE 是读写。程序是写在纸带上的规则。它自己也是符号,所以它可以被读,也可以被另一段规则改掉。


传统路径把规则和纸带拆开了

C 把规则写成人类读得懂的文本。指针模拟纸带上的任意一格,循环模拟头的来回走,函数和递归模拟状态的转移。然后编译器把文本变成语法树,再变成中间表示,再变成机器码。CPU 执行机器码,改的是内存。

这条路很快。它不适合一个只会对符号序列做统计性改写的模型。语法空间很大:关键字、类型、模板、异常、继承,每样都有自己的形状。运行时的状态是碎的:虚表、异常帧、分配器的元数据,散在各处。代码是一份文件,跑起来的对象是另一份。模型在高维里轻轻推一下,落回这套离散的语法和运行时,可能是一个能跑的改动,也可能是一处幻觉、一个语法错误,或者一处把整棵语义弄断的改动。

模型产出的是一段新文本。外面再编译、再跑、再把报错送回去。精度靠 diff。反馈是整段重来。改完之后,没有一个稳定的编号回答:这一格还是刚才那一格吗。


Lisp 把语法收成一条,还差最后一步

Lisp 的表面几乎只有一条规则:一个运算符,后面跟着参数。S 表达式写下来就是树。代码和数据是同一种东西。生成出来的表达式可以 quote,可以 eval,可以展开成宏,中间不必再发明一种解析。

运行时也可以收得很小。一张环境,一片堆,一条栈。eval 和 apply 是运行时看得见的动作。模型在高维里的扰动,落到这么小的语法上,比较容易仍然是一棵合法的树。改动也容易只碰局部。REPL 里可以马上试。

常见的 Lisp 仍差一步。路径多半是 read 一段文本,得到一棵新的列表,再 eval。你手里上一次那个 cons 不一定还在。编辑器里的 S 表达式,和镜像里正在跑的对象,常常仍是两份。同像性说明代码可以当数据。它还没有说明:正在跑的那一格,改完以后还是那一格。


Aura 把纸带留在工作区里

Aura 用 Lisp 的表面,把纸带做成正在跑的工作区。

工作区里是一棵扁平的语法树,FlatAST。节点有编号。查询按编号走,改写按编号写。编号就是读写头停住的地址。

四件事对应纸带上的四种动作。

纸带 工作区
读一格 query,按名字或按节点号找到那一格
写一格 mutate:rebind、mutate:replace-value,改这一格的内容
走一步 eval-current,按现在这棵树求值
先描一份再涂改 ast:snapshot,不行就 ast:restore

set-code 只在装入的时候用一次,把一段源码放进工作区。之后的修改不把整段文本再解析成一棵新树。果果编程课把这件事演示到了一格:七个字符串字面量在启动时钉住,翻一张卡片只对其中一个编号做 mutate:replace-value。定义号还在,字面量号还在,变的是引号里的字。0.1.0 自带的 evolve 是同一条路,粒度是一个定义:mutate:rebind 换掉 params 的身体,门禁失败就整段恢复。

这三句话是设计原则,用跑起来的动作说。

语言接住细节。模型或人给出要改的名字、候选的源码、一句理由。解析、装入、求值、记日志,是运行时的事。不需要模型自己在文本上算行号。

代码即记忆。Agent 能读、能写、能核对的那份结构,就是正在跑的程序,不是旁边另存的一份状态。工作区可以整段存下来,文件以 AURASOUL 开头。果果页面上的灯和卡片不是浏览器里的第二份真值。evolve 最后印出的 params 也不是日志里抄下来的数,是求值此刻这个定义得到的值。

闭环要能撤回。query 是感知,mutate 是行动,eval-current 是验证,ast:snapshot 和 ast:restore 让一次实验可以当作没发生过。下一步试哪一个候选,可以是 evolve 里那种随机的一小步,也可以是标准库里的搜索。信息素、控制量,是选候选的策略,不是这一刀本身。刀是:改一格,跑一下,不合适就放回去。

高维里的意图就落在这里。模型可以仍在嵌入空间里决定「想要另一版 fact」或「这一位应该是 1」。Aura 不把这个意图当成一整文件的替换。它收成一次指名的、局部的写,写完用同一次求值看结果,失败则纸带回到快照。CPU 最后执行的仍是确定性的符号变换。变的是转移规则里的一格,而且这一格的编号还在,所以下一次还能指着它说:上一笔写在这里。


和前两篇是同一件事

第一篇指出那个在外面打转的循环:生成文本,外面跑一下,再生成。精度、速度、撤回,三件事都缺。

果果把循环收成一个孩子看得见的动作。页面和程序是同一份字面量。编号不变,所以页面可以指着同一格说,这一笔是 replace-value。那一课没有 agent:spawn,没有多 Agent 管线,也没有在课上长出新节点。它要证明的是最小的闭环可以小到一个字符串。

0.1.0 把这个运行时收成可以安装的一版。evolve 用同一个闭环改一个定义,四十步,每步留下或撤回。树的形状在这个例子里不动。栈、堆、指令序列那种会增减节点的改写,要用 mutate:splice 一类操作,插完还得重新钉住新编号。开关课故意不碰。0.1.0 的例子也不碰。

和「改一个字就重读、重解析、重装整份源文件」比,这是节点上的写。和「语言自动找出所有读过这个绑定的地方,并只重做那些地方」比,这一版的例子还没有走到那一步。evolve 的工作区里,被改的就是 params 自己。适不适合,由旁边的 fitness 重算,不是编译器去重编一棵调用图。


代码即记忆

0.1.0 没有把接口冻住。它把纸带放到了你可以装上的运行时里。

装 v0.1.0,跑 aura new evolve,看 params 在工作区里换了哪几步。再打开果果那一课,看同一刀细到一个字符串节点时,画面和程序怎么一起动。

代码即记忆。计算要进化的,是写在纸带上的规则。改完以后,那一格还在。