原来所有边都交给 React Flow 的 smoothstep,它的竖直段一律落在源和目标的
正中间。同一列的边于是全挤在同一个 x 上:一张"入口扇出三路 → 归并 → 分支
→ 出具/驳回"的图上,跨层的 a3→join 和 m2→join 的横段**完全压在同一条线
上**,长边还直接从"归并B"卡片中间穿过去。排版那边同样糙:同层按 id 字典序
竖排,所以 out_no 排在 out_yes 上面("n" < "y"),分支的 yes/no 是反的;
层内没有任何减少交叉的排序;层间距写死 300。
后端 app/engine/layout.py 重写成分层布局:先剔回边再按最长路径分层(loop
的回边不参与分层,否则循环体会被压到循环节点左边);层内中位数启发式上下扫
六轮取交叉最少的一版,分支/循环的出口再按声明顺序占位重排;y 先均匀堆叠再按
邻居重心迭代松弛,同列节点因此和上下游对齐,长边也走得直;最后按"穿过这条
走廊的连线在竖直方向最多重叠几条"给走廊留宽——区间图上的贪心染色,和前端
用的是同一套,走廊留窄了前端再聪明也只能把线叠起来。
前端 canvas/routing.ts 自己算走线:出边沿右边缘错开、入边沿左边缘错开(入边
按上游纵向位置排序),竖直段按区间染色分配车道——真叠在一起的才各占一条,
首尾相接的(同一个扇出点)共用一条,看起来就是一条总线;跨层的长边和循环的
回边在中间各列的纵向占用里找空档绕过去,不再压过卡片。边换成自定义组件后
React Flow 不再自动加动画类,实测确认 .edge-active 仍然生效。
顺带修掉"排完版看不见图":整张图换过坐标后(自动排版 / Copilot 生成完 /
切图)自动重新取景。坐标变了视角没变,人盯着的是一片空白。
新增 scripts/check-canvas-layout.mjs:把夹具图交给真后端 /api/copilot/layout
排版,再直接 import('/src/canvas/routing.ts') 用页面上跑的那份代码算路径,
按几何断言没有两条线压在同一段上、没有线穿过别人的卡片、两端都接在节点边缘,
另有 1325 条随机坐标的 fuzz 防手拖之后算出 NaN 路径(NaN 的 path 什么都不
画,界面上就是"线没了",一声不吭)。这条检查做过变异验证——把 laneX 改成
永远返回走廊中点,它立刻报 a3→join × m2→join,正是原图上那处重叠。
285 passed(新增 12 条排版不变量)。check-ui 69 项通过,唯一失败项"本地哈希
要标出没有语义能力"已 git stash 到基线复现,与本次无关。
24 KiB
AgentLab
可视化 Agent 编排实验台——在画布上定义工作流,运行时逐节点观察执行过程, 并对产出施加可验证的约束。
面向工业场景的受限动态编排:模板定义骨架,agent 只在被授权的关节处自由。 所有算术下沉到受控指标集,叙述层无权创造数字;正式运行钉死在不可变版本上, 每一次执行的完整证据落进内容寻址的工件库。
研究助手模板执行中:已完成的节点标绿、执行中的节点高亮并显示进度, 右栏是同一份事件流驱动的实时时间线。
目录
快速开始
环境要求:conda(miniforge 即可)、Node 20+、pnpm。 无需 Docker——代码沙箱基于操作系统自带的隔离原语,可选升级至 microVM。
conda env create -f environment.yml # 首次:创建 agentlab 环境并安装依赖
./scripts/dev.sh # 之后:一条命令启动前后端
dev.sh 自动定位 agentlab conda 环境;环境不存在时依 environment.yml 创建。
如需使用其他环境名,设置 AGENTLAB_CONDA_ENV。
服务启动于 http://localhost:5273 。
无需配置 API Key 即可完整体验。 内置 Mock provider 产出流式回复、工具调用与
符合 schema 的结构化数据,编排链路(画布高亮、审批中断、沙箱执行)会完整走一遍。
接入真实模型请在「设置 → 模型接入」配置。启动时若环境变量中存在
ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / OPENAI_API_KEY,将自动导入为 provider。
内置 8 个模板,从「最小问答」到「周报(口径卡 + 三档出具)」,既可直接运行, 也是各类节点用法的可执行文档。
核心能力
| 领域 | 能力 |
|---|---|
| 可视化编排 | 16 种节点拖拽连线;运行时节点实时高亮、边上数据流动画、卡片内直接呈现流式 token |
| 对话式取数 | 「问数据」页:自然语言提问,系统自行接入数据源、生成工作流、执行并给出结论;对话持久化、可分享链接 |
| 多模型接入 | Anthropic / OpenAI / 任意 OpenAI 兼容服务(DeepSeek、Kimi、通义、智谱、硅基流动、Ollama 等)+ Mock |
| 工具链 | 15 个内置工具 + 自定义工具(HTTP 模板 / 沙箱 Python)+ MCP server 接入 |
| 代码沙箱 | 双档隔离:系统沙箱(Seatbelt / bubblewrap,冷启动约 25ms)或 microVM(独立 Linux 内核,内存限额真实生效) |
| 人工介入 | 执行暂停并落盘,人工批准 / 驳回 / 改稿后从断点继续;进程重启不丢状态 |
| 断点续跑 | 失败的运行可从失败节点继续,已完成的节点不重复执行;允许修改节点配置,但拓扑必须一致 |
| 知识库 | 向量 + BM25 混合检索、倒排索引、可选模型重排;PDF / Word / PowerPoint / HTML / Markdown 解析;向量模型可接本地端点 |
| 表格入库 | Excel / CSV 上传后转为可用 SQL 查询的表,自动生成 db_query__<name> 工具——数字由 SQL 计算得出,而非模型读取 |
| 长期记忆 | 跨运行的记忆读写与自动去重;Copilot 建图时主动记录应当记住的事实 |
| Skill 管理 | 将方法论抽象为可复用单元,挂载到节点上注入 system prompt |
| 自然语言编排 | Copilot:一句话生成或改写整张图,自动排版与校验 |
| 结构化成果 | JSON Schema 校验,不合格时将错误回传模型自动返工 |
| 产出复核 | 运行结束后规则扫描事件流,发现异常才调用模型复核;复核可重组答案但不得引入原答案中不存在的数字 |
| 追踪与成本 | 全量事件落库可回放;逐节点耗时、token 用量、按目录价折算的成本 |
| 三档出具 | 口径卡(受控指标集)+ 出具契约:叙述中每个数字回指指标集,formal / degraded / withheld 三档判定 |
| 正式 / 探索分级 | 正式运行仅从已发布的不可变版本发起(按图哈希钉死);画布试跑自动标记为探索性 |
受限动态编排:模板定骨架,agent 填关节
工业场景中大多数任务的步骤是已知的(接单报价、周报、体检)。让模型每次现场重新规划, 既是在重复发明已知答案,也使结果不可复现——而不可复现即不可验收。
AgentLab 的引擎是编译式的:运行时模型无法修改图结构,其自由被结构性地限制在 agent 节点内部与 branch 决策之中。在此基础上划分三层:
flowchart LR
T["<b>模板层</b> · 骨架固定<br/><br/>已发布的不可变版本<br/>WorkflowVersion + 图哈希<br/>governed 级须过治理 lint"]
J["<b>关节层</b> · 有边界的自由<br/><br/>agent 工具白名单<br/>branch 语义分类<br/>缺数据时降档"]
E["<b>探索层</b> · 不进正式归档<br/><br/>画布试跑 / 临时图 / 追问<br/>跑通的路径提取为草稿"]
T ==>|约束| J
E -.人工审核后发布.-> T
- 模板层 正式运行只认已发布的不可变版本。发布后修改画布不影响已发布版本。 governed 级发布须通过治理 lint:禁用 supervisor(全动态归属探索层)、方法卡必须钉版本、 agent 不得关闭全部审批、output 必须配置出具契约。
- 关节层 授权范围内的自由。工具白名单衰减报表(
/api/governance/tool-usage) 将「已授权但从未使用」的工具显性化,为收缩权限提供依据。 - 探索层 结果自动标记为探索性,不进正式归档。跑通的路径经「提取模板」沉淀为草稿 (仅保留实际执行的节点,剪除未走的分支),人工审核后发布。探索问题聚类提示哪些追问 应当固化为模板中的一节。
出具链路
模板⑧是完整示范。设计前提是派生边界收死:所有算术发生在口径卡节点内,叙述层只能引用。
flowchart LR
A["取数<br/><i>固定步骤</i>"] --> B["口径卡<br/><i>受控指标集</i>"]
B --> C["叙述<br/><i>LLM</i>"]
C --> D["出具契约<br/><i>数字回指校验</i>"]
D --> E1["formal"]
D --> E2["degraded"]
D --> E3["withheld"]
A -.快照.-> S[("工件库<br/>sha256 寻址")]
B -.口径名 + 版本.-> S
校验器从叙述中抽取全部数字逐个回指指标集,支持千分位、百分数与比率互转、万/亿后缀、 按书写精度的舍入容差;日期、枚举序号、引用标记不参与回指。三档判定与缺数据声明随成果印出。
实测记录:Opus 5 撰写的正文数字全部回指成功,但结尾「(约 100 字)」中的 100 被判定为
集合外数字并触发降档。处置方式是修改 prompt 而非扩大白名单——这正是校验器应有的行为。
口径升版是被迫处置事件。 钉住的方法卡出现新版本时,正式运行将被拒绝启动,
直至模板显式声明升版策略:recompute(回算历史)/ dual(双印)/ incomparable
(标注不可比)。此处无默认值——口径变更最容易静默翻车的形态是「数字看起来都对,
只是与上周不可比」。
每次运行的完整证据存于内容寻址的工件库(data/artifacts/,sha256 即地址,取回时复验哈希);
运行终态的事件清单哈希记录在 Run 上,事后修改流水将无法对齐。
工作流形态
人工介入与分支——执行在 human 节点暂停并落盘,人工决策后沿对应出口继续:
多 Agent 协作——调度者按进展将任务分派给多个专家,属探索层能力,governed 级发布禁用:
节点间通过模板语法传递数据:
{{ input.question }} 入口输入
{{ vars.answer }} 某节点 assign_to 写入的变量
{{ nodes.n1.text }} 指定节点的输出
{{ vars.items | json }} 过滤器:json / compact / upper / length / first …
连线即并行:单个节点连出多条边构成 fan-out,多条边汇入同一节点时自动等待汇聚。
自动排版按拓扑分层:并行的支路落在同一列,层内按中位数启发式排序以减少交叉,
分支节点的出口按 yes/no 的声明顺序自上而下,循环的回边不参与分层(否则循环体会
被压到循环节点左边)。层与层之间按"穿过这条走廊的连线在竖直方向最多重叠几条"
预留走廊宽度,前端 canvas/routing.ts 据此给每条线分配独立的竖直车道——扇出、
汇聚的线因此不会叠在同一段上,跨层的长边则从没有节点的空档绕过去。
节点类型
| 分类 | 节点 | 说明 |
|---|---|---|
| 起止 | input output |
声明输入字段 / 收集结构化成果 |
| 模型 | llm agent supervisor |
单次调用 / 带工具循环 / 多 agent 协作 |
| 执行 | tool code |
直接调用工具 / 沙箱内执行代码 |
| 控制 | branch loop subgraph |
条件或语义分支 / 遍历与条件循环 / 嵌套工作流 |
| 上下文 | memory retrieve transform |
长期记忆读写 / 知识检索 / 数据整形 |
| 把关 | human validate metrics |
人工介入 / Schema 校验与自动返工 / 口径卡 |
架构
flowchart TB
subgraph FE["前端 · React 19 + React Flow + Zustand"]
F1["画布编辑"]
F2["问数据"]
F3["运行追踪"]
F4["decode.ts<br/><i>事件 → 人类可读的唯一翻译层</i>"]
end
subgraph BE["后端 · FastAPI + LangGraph"]
B1["engine<br/>编译 · 调度 · 中断恢复"]
B2["providers<br/>多模型接入与成本估算"]
B3["tools<br/>注册表 · MCP · 数据源"]
B4["memory<br/>记忆 · 知识库 · 混合检索"]
B5["sandbox<br/>microVM / Seatbelt / bubblewrap"]
end
subgraph ST["存储"]
S1[("agentlab.db<br/>业务数据")]
S2[("checkpoints.db<br/>执行断点")]
S3[("artifacts/<br/>内容寻址工件库")]
end
FE <-->|REST + WebSocket| BE
B1 --> S1 & S2 & S3
backend/ FastAPI + LangGraph
app/engine/ 画布 JSON → StateGraph 的编译与执行
schema.py 图定义与静态校验
compiler.py 编译:节点包装、事件、重试、条件路由
runner.py 执行调度、事件总线、中断与恢复
issuance.py 出具契约:数字回指与三档判定
review.py 产出复核:事件流规则扫描 + 模型复核
expressions.py 模板插值 + AST 白名单表达式求值
nodes/ 各类节点的执行器
app/data/ 数据源接入:连接、SQL 守卫、结构探查、表格导入
app/providers/ 多模型接入与成本估算
app/sandbox/ microVM / Seatbelt / bubblewrap / 裸子进程四后端
app/tools/ 工具注册表、内置工具、MCP、自定义工具
app/memory/ 记忆、知识库、倒排索引、混合检索、文档解析
app/api/ REST + WebSocket
frontend/ React 19 + React Flow + Zustand
src/canvas/nodeDefs.ts 节点元数据——属性面板、节点库、连接桩均由其驱动
src/canvas/routing.ts 连线走线:端口错开、走廊车道、长边绕行
src/run/decode.ts 事件流 → 人类可读步骤的唯一翻译层
src/store/studio.ts 画布状态 + 事件流到高亮的映射
environment.yml conda 环境定义
scripts/dev.sh 一键启动
scripts/check-*.mjs 前端验证脚本
关键设计决策
选用 LangGraph
可视化编排的难点不在绘制节点与连线,而在暂停、恢复与回放。LangGraph 的 checkpointer 与
interrupt() 将这几件事做在了底层:人工介入是一次中断加一次 Command(resume=...),
进程重启后仍可从断点继续;aget_state_history() 直接给出每一步的状态快照。
由此派生的第二项收益:agent 内部的工具审批不重复计费。interrupt() 会使整个节点重放,
朴素实现将重复执行此前的模型调用。agent 节点内的模型调用与工具执行均包裹在 LangGraph 的
@task 中,结果进入 checkpoint,重放时直接读取缓存。
事件流是唯一的可视化数据源
节点通过 get_stream_writer() 发出事件,runner 转换为 RunEvent 落库并广播。
画布高亮、token 流、工具卡片、时间线全部由同一份事件驱动——刷新页面或中途接入
均可获得完整过程(WebSocket 先补历史再接实时)。
前端侧对应地只有一个翻译层 run/decode.ts:画布右栏、问数据页、运行详情页共用它。
分设多套的后果是同一次运行在三个页面讲出三个版本,而使用者无从判断哪个为真。
沙箱:双档隔离,以及必须声明的缺口
轻量档使用操作系统自带的访问控制:macOS 的 Seatbelt(sandbox-exec)、Linux 的 bubblewrap,
冷启动约 25ms,无需额外安装。重量档为 microVM(libkrun + Hypervisor.framework),
启动带独立内核的虚拟机。
两档共同保证三项约束:默认断网、家目录不可读、仅工作区可写。Seatbelt 侧策略
由内核强制且对子进程继承——沙箱内 subprocess 启动的 cat 读取家目录同样返回
Operation not permitted;解释器刻意使用 sys.base_prefix 下的干净 Python 而非项目环境,
因此沙箱代码无法 import FastAPI、SQLAlchemy 及任何凭据处理相关模块。
Seatbelt 的局限:它提供访问控制而非虚拟化。进程表共享(可见宿主机进程列表);
代码以当前用户身份运行(不似容器会降权至 nobody);macOS 不强制 RLIMIT_AS,
内存用量无法限制(实测设定 256MB 仍可分配 900MB)。CPU 时间、单文件大小、
墙钟超时三项有效。
microVM 补齐的正是内存这一项。 本机实测:宿主为 Darwin 27.0.0,VM 内为 Linux 6.12.99
(libkrunfw 编译),PID 1 为 init.krun,/Users 在 VM 内不存在,free 仅显示 512MB。
申请 900MB 将被内核 OOM killer 终止而 VM 存活——RLIMIT_AS 在 macOS 上形同虚设的问题
在此不是被修复,而是结构上不存在。两台 VM 之间亦互不可见。
代价:运行时约 50MB,OCI 镜像首次拉取实测 54.5 秒;缓存后热启动 0.19 秒、 VM 内执行 7–30ms。成本集中在首次而非每次——因此 auto 模式在镜像未缓存时优先使用 Seatbelt, 避免一分钟的等待(显式指定 strict 不受此限)。
网络隔离不完整,此项须置于前列。
network=false时 HTTP/HTTPS 与域名解析均被切断,但 UDP/53 无法拦截——实测手写 DNS 包仍可获得真实响应。default_egress=DENY、Rule.deny_dns()、显式 deny UDP、max_connections=0均已尝试,UDP 通道始终无法封堵(deny_dns生成的规则仅作用于宿主, DNS 出口在 microsandbox 的网络栈中硬编码放行)。 即 DNS 隧道外泄通道始终开放。该隔离足以防范「代码意外联网安装依赖」, 不足以防范「恶意代码窃取数据」。设置页据此将网络一项标注为「部分生效」的黄色而非绿色—— 拦截了一半却显示为全绿,比显示为全红更危险。真正的外泄防护须在网络层实施。
检索为何是混合的
默认 embedding 为本地特征哈希:不联网、不下载模型、零配置可用,但语义泛化能力弱。 BM25 补齐精确关键词匹配。两者各自归一化后加权,权重在界面上可调,命中结果分别显示 语义分与关键词分。
实测(24 文档 / 16 问题评测集,hit@1):
| 配置 | hit@1 | 字面类 | 语义类 |
|---|---|---|---|
| 纯关键词(alpha=0) | 88% | 100% | 75% |
| 纯向量(本地哈希,alpha=1) | 81% | 88% | 75% |
| 混合 + Qwen3-Embedding-4B | 94% | 100% | 88% |
| 上述配置叠加模型重排 | 100% | — | — |
值得注意的一项:纯向量(81%)低于纯关键词(88%),混合才达到 94%。专有名词依靠 BM25、 同义改写依靠向量,两者各担一半——混合检索在该语料上是必要的,而非装饰。 样本量为 16 题,单题即 6 个百分点,结论的方向可信而精度不可过度解读。
向量模型可接任意 OpenAI 兼容端点(LM Studio、Ollama、vLLM、TEI)。维度通过实际探测获得 而非按模型名推断——本地部署的模型维度各异(bge-m3 为 1024、Qwen3-Embedding-4B 为 2560), 推断错误的后果不是偏差,而是全部存量向量被判定为与当前 embedder 不匹配,检索整体退回关键词。
表格数据必须成为表
Excel / CSV 若传入知识库,只能被切块检索,数字即成为模型从片段中「读取」的结果。 而本项目的基础约定是「所有算术下沉到 SQL 或口径卡」——产出复核层能拦截凭空出现的数字, 无法拦截「从一堆片段中读错了一格」。
因此表格上传后落为 SQLite 文件并注册为 kind=sqlite 的数据源,自动生成
db_query__<name> 工具。列类型逐列推断(INTEGER / REAL / TEXT),空单元格存为 NULL——
整数列若落为 TEXT,ORDER BY 将退化为字典序('10' < '9'),且不产生任何报错。
安全边界
- 分支条件使用 AST 白名单求值器而非
eval - HTTP 工具解析域名后逐 IP 检查,拦截内网、回环与云元数据地址,且不跟随重定向(逐跳重新校验)
- 文件工具路径
resolve()后必须仍位于工作区内 - SQL 守卫采用白名单动词判定 + 多语句拦截 + 结果集上限;
DROP/TRUNCATE/GRANT等 在可写数据源上亦一律拒绝 - API Key 经 Fernet 加密落库,仅向前端返回掩码
运行环境
沙箱后端选择
设置页「运行环境」列出全部候选及其可用性,并标注当前选中项。安装 microVM 依赖且镜像已缓存时
auto 选择 microvm;否则 macOS 使用 seatbelt、Linux 上安装 bwrap
(apt install bubblewrap)后使用 bubblewrap;均不可用时降级至 local——该路径不含任何
访问控制,界面明确标红。可通过 AGENTLAB_SANDBOX_BACKEND 强制指定,
亦可在代码节点上单独选择隔离档位(strict = microVM,fast = 系统沙箱)。
启用 microVM
三项准备,均为一次性——日常启动仍只需 ./scripts/dev.sh:
- Python 包:使用
environment.yml创建环境时已包含(含[microvm]extra) - 运行时(约 50MB,落于
~/.microsandbox):每台机器一次 - OCI 镜像(实测约 55 秒):首次执行代码时自动拉取,每个镜像一次
仅第 2 项需手动执行:
conda run -n agentlab python -c "import asyncio,microsandbox as m; asyncio.run(m.install())"
三项均落于磁盘,重启机器或重开终端无需重来。设置页「运行环境」中 microvm 条目的
warm 为 true 即表示三项齐备,auto 将自动升级至 microvm。镜像可通过
AGENTLAB_MICROVM_IMAGE 更换,闲置 VM 回收时间由 AGENTLAB_MICROVM_IDLE_SECONDS 控制。
可选依赖
pip install -e 'backend[docs]' # PDF / Word / PowerPoint / Excel 解析
pip install -e 'backend[db]' # MySQL / PostgreSQL / Oracle 驱动
pip install -e 'backend[microvm]' # microVM 沙箱
pip install -e 'backend[dev]' # pytest
数据位置
全部位于 data/:agentlab.db 业务数据、checkpoints.db 执行断点、
artifacts/ 工件库、workspace/ 沙箱文件、uploads/ 上传文件、.secret_key 加密密钥。
删除 data/ 即恢复出厂设置。
常见问题
模型返回空内容。 Claude 4.6 之后的模型默认启用 thinking,max_tokens 过小会出现
「思考耗尽额度,正文为空」。将节点的 max_tokens 调大,或将思考模式设为关闭。
时间线中会给出相应提示。
中转网关返回 401。 部分网关仅接受 Authorization: Bearer,且额外携带 x-api-key
即会拒绝。在 provider 设置中勾选「用 Authorization: Bearer 认证」。
数据源结构探查失败。 探查失败与「尚未探查」是两种状态:前者点击按钮无法解决,
需先处理超时或更换 schema。数据源卡片会显示失败原因与该服务器上的可选库列表。
agent 侧同样会收到实情,并被告知可直接查询 information_schema 自行确认表结构。
端口冲突。 前端默认 5273、后端 8000,通过 AGENTLAB_WEB_PORT / AGENTLAB_PORT 修改。
开发与验证
# 后端:285 个测试
cd backend && pytest tests
# 前端类型检查
cd frontend && npx tsc -b
# 前端验证(需先启动 ./scripts/dev.sh)
node scripts/check-decode.mjs # 事件 → 步骤的翻译是否正确
node scripts/check-stream.mjs # 助手流组件渲染是否正确
node scripts/check-ui.mjs # 真浏览器端到端:URL 寻址、会话、运行详情
node scripts/check-canvas-layout.mjs # 排版与连线几何:线有没有叠、有没有穿过卡片
node scripts/e2e-check.mjs # 冒烟:打开画布 → 选模板 → 运行 → 观察高亮
三层前端验证的分工:check-decode 守「翻译对不对」,check-stream 守「画出来对不对」,
check-ui 守「整条路走得通不通」。三者可各自通过而合起来是坏的——例如 Step 字段全部正确,
组件却将其渲染为一屏转义 JSON。
check-canvas-layout 单独守「图好不好看」里唯一能算的那部分:它把夹具图交给后端
/api/copilot/layout 排版,再直接 import('/src/canvas/routing.ts') 用页面上正在跑的那份
走线代码算路径,然后按几何断言——没有两条线压在同一段上、没有线从别人的卡片中间穿过去、
每条线的两端都接在节点边缘。连线叠成一团时 check-ui 和 e2e-check 一个字都不会说。
License
This project is source-available and is licensed under the PolyForm Noncommercial License 1.0.0.
Noncommercial Use
You may use, study, modify, experiment with, and develop upon this software for purposes permitted under the PolyForm Noncommercial License 1.0.0.
Typical permitted noncommercial uses include:
- personal research;
- learning and education;
- experimentation;
- evaluation and testing;
- hobby projects;
- noncommercial software development.
See the LICENSE file for the applicable license terms.
Commercial Use
Commercial use is not permitted without prior written authorization from Yilun JIANG.
If you intend to use this software in a commercial product, commercial service, SaaS offering, customer project, commercial deployment, or other commercial context, you must obtain a separate commercial license before doing so.
For commercial licensing, see COMMERCIAL-LICENSE.md or contact:
Yilun JIANG me@thearchyhelios.com
Copyright
Copyright (c) 2026 Yilun JIANG.
All rights reserved except as expressly granted under the applicable license.



