Archon Script Nodes 实战指南:用 TypeScript 与 Python 构建确定性 DAG 节点
【免费下载链接】ArchonThe first open-source harness builder for AI coding. Make AI coding deterministic and repeatable.项目地址: https://gitcode.com/GitHub_Trending/archon3/Archon
Archon 的 DAG 工作流(Workflow)节点支持一个script字段,让你直接用 TypeScript、JavaScript 或 Python 片段作为工作流的一环执行,全程不调用任何 AI Agent。脚本由bun或uv运行时驱动,stdout被捕获为节点输出,并可在下游以$nodeId.output引用。阅读本文后,你将掌握脚本节点的全部配置参数、内联代码与命名脚本的判定规则、output_format结果契约、依赖管理与环境隔离机制,并能组合出“AI 产出 → 脚本清洗 → 确定性后续处理”的高质量流水线。
脚本节点适合承担确定性的“真编程”工作:解析 JSON、在两个 AI 节点之间转换数据、用类型化客户端调用 HTTP API、或计算 shell 一行命令难以表达的值。如果一条普通 shell 命令就足够,请优先使用bash:节点;只有在需要完整编程语言能力时才引入脚本节点。
快速上手:三种基本写法
内联 TypeScript(bun 运行时)
nodes: - id: parse script: | const data = { count: 42, label: "ok" }; console.log(JSON.stringify(data)); runtime: bun内联 Python(uv 运行时)
nodes: - id: compute script: | import json, statistics values = [1, 2, 3, 4, 5] print(json.dumps({ "mean": statistics.mean(values) })) runtime: uv引用.archon/scripts/下的命名脚本
nodes: - id: fetch-pages script: fetch-github-pages # resolves .archon/scripts/fetch-github-pages.ts runtime: bun timeout: 60000文件.archon/scripts/fetch-github-pages.ts会被加载,并以bun --no-env-file run <path>执行。
工作原理:一次脚本节点执行的完整生命周期
- 变量替换:执行前,
$ARGUMENTS、$WORKFLOW_ID、$ARTIFACTS_DIR、$BASE_BRANCH、$DOCS_DIR以及上游$nodeId.output引用会被替换进script文本。 - 内联 vs 命名判定:若
script值包含换行或任何 shell 元字符,则视为内联代码;否则按命名脚本引用处理(详见内联与命名脚本)。 - 分发执行:
runtime: bun+ 内联 →bun --no-env-file -e '<code>'runtime: bun+ 命名 →bun --no-env-file run <path>runtime: uv+ 内联 →uv run [--with dep ...] python -c '<code>'runtime: uv+ 命名 →uv run [--with dep ...] <path>
- 结果捕获:
stdout(去除尾部换行)成为$nodeId.output。成功运行时,stderr仅记录为警告并发布到对话,不会使节点失败。非零退出码则使节点失败;失败时两条流的尾部会出现在错误信息中(Script node 'X' failed [exit N]: [stderr] ... [stdout] ...),共享约 2 KB 诊断预算——stderr 优先,stdout 取剩余部分,且仅当两条流都非空时才加前缀标签。若 stderr 为空,stdout 尾部即作为诊断内容。脚本正文永远不会回显给用户。超时默认也判定失败;若结果可选且下游节点通过if_skipped绑定处理缺失,可设on_timeout: skip。 - 证据保留:无论成败,两条流都会以脱敏、限长后的尾部写入运行转录,形成
exec_output行(见保留的子进程证据)。该保留仅作证据,永不截断$nodeId.output。
这套分发逻辑可在源码 packages/workflows/src/dag-executor.ts 中直接验证:内联分支用['--no-env-file', '-e', finalScript](bun)或['run', ...withFlags, 'python', '-c', finalScript](uv);命名分支则按scriptDef.runtime分发到uv run [--with ...] <path>或bun --no-env-file run <path>,其中--no-env-file专门用于阻止 Bun 自动加载执行目录(目标仓库)下的.env。实现还通过PYTHONDONTWRITEBYTECODE: '1'禁用 CPython 字节码缓存,避免导入缓存污染冻结源码(dag-executor.ts)。
YAML Schema 与字段详解
- id: node-name script: <inline code OR named identifier> # required, non-empty runtime: bun | uv # required deps: ["httpx", "pydantic>=2"] # optional, uv-only (见下文) timeout: 60000 # optional ms, 默认 120000 on_timeout: skip # optional; 默认是失败 depends_on: [upstream] # optional when: "$upstream.output != '[]'" # optional (upstream 是 bash/script 节点; # AI 生产者需要 output_format + 字段) output_format: # optional JSON Schema; 让 stdout 成为契约 type: object properties: severity: { type: string } required: [severity] trigger_rule: all_success # optional (default) retry: # optional; 与 bash/AI 节点同构 max_attempts: 3 on_error: transient字段速查表
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
script | string | 是 | 内联代码,或所属工作流scripts/目录下的命名脚本(打包工作流)或共享脚本目录(旧式工作流) |
runtime | 'bun'|'uv' | 是 | 执行脚本的运行时;命名脚本必须与文件扩展名匹配 |
deps | string[] | 否 | 本次运行安装的 Python 依赖。仅 uv 生效——bun 下会忽略并给出警告 |
timeout | number (ms) | 否 | 超过该毫秒数后强制终止。默认120000(2 分钟) |
on_timeout | 'skip' | 否 | 超时后以“跳过”状态完成节点,默认为失败。持久化的跳过原因为timeout |
output_format | object | 否 | 节点 stdout 必须满足的 JSON Schema,见声明结果契约 |
标准 DAG 字段(id、depends_on、when、trigger_rule、retry)全部可用,output_format同样可用。AI 专属字段(model、provider、context、allowed_tools、denied_tools、hooks、mcp、skills、agents、effort、maxBudgetUsd、systemPrompt、fallbackModel、betas、sandbox)会被解析器接受,但会触发加载器警告并在运行时忽略——脚本节点不会调用任何 AI。idle_timeout同样被接受但忽略:脚本节点是一次性子进程,请改用timeout(N 毫秒后硬杀)。
内联与命名脚本(Inline vs Named Scripts)
执行器直接根据script字符串本身决定模式:包含换行或任何 shell 元字符即为内联代码,否则按命名脚本查找。
- 触发内联模式的元字符:空格,
;(){}&|<>$`"' - 内联示例:
"const x = 1; console.log(x)"、多行代码块、任何含空格的片段 - 命名示例:
fetch-pages、analyze_metrics、triage-fmt——无空白、无 shell 语法的裸标识符
如果你想要一段恰好语法上是单个标识符的内联代码,追加一个尾注释或换行即可强制进入内联模式。
该判定的实现位于 packages/workflows/src/executor-shared.ts 的isInlineScript:script.includes('\n') || /[;(){}&|<>$"' ]/.test(script)`,被 DAG 执行器与验证器共用,保证运行时与校验行为一致。
命名脚本解析
命名脚本使用两种解析模式之一:
- 打包工作流:仅从所属工作流的
scripts/目录解析。捆绑包内脚本使用相同的归属规则,并在二进制发行时内嵌。 - 旧式工作流:从
<repoRoot>/.archon/scripts/再至~/.archon/scripts/解析共享脚本。
工作流本地查找限定在声明该节点的工作流范围内(包括通过include:展开),作者仍然只写裸名称(script: publish),归属键是内部实现细节。同名冲突时仓库本地条目静默胜出,详见全局工作流的共享优先级规则。
从源码看,脚本发现实现在 packages/workflows/src/script-discovery.ts:discoverScriptsForCwd依次合并 bundled 打包脚本、home 共享/打包脚本、仓库共享/打包脚本(仓库覆盖 home),打包脚本使用所有者限定的内部键;共享目录每个子文件夹只下探一层(如.archon/scripts/triage/foo.ts解析为foo),更深层嵌套被忽略(script-discovery.ts 的MAX_SCRIPT_DISCOVERY_DEPTH = 1),同名脚本跨扩展名重复会直接抛错。
在包(Pack)内共享代码
把可复用的.ts、.js或.py模块放在<pack>/.shared/下,支持模块子目录。请使用普通文件——二进制生成器拒绝.shared下的符号链接。.shared专为模块保留:其中的文件既不是工作流,也不是命名脚本目标。命名了不可用脚本(包括共享模块)的打包工作流会在加载时失败。
my-pack/ ├── .shared/ │ ├── result.ts │ └── result.py ├── release/ │ ├── release.yaml │ └── scripts/ │ └── publish.ts └── inspect/ ├── inspect.yaml └── scripts/ └── report.pyBun 侧从脚本位置导入:
// release/scripts/publish.ts import { summary } from '../../.shared/result.ts'; console.log(summary);Python 脚本以文件方式运行,from ...shared这类包相对语法不适用。用 Python 标准库把包的共享目录加入路径:
# inspect/scripts/report.py from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).resolve().parents[2] / ".shared")) from result import summary print(summary)Archon 会在项目与全局源码树、捆绑二进制和冻结快照中保留这些相对路径。二进制会把每个包的脚本与模块作为一个单元缓存;修改任一共享模块都会生成新单元。作者编写的脚本仍是唯一入口,因此工作流节点依然使用script: publish这样的名称。
请把输出写到提供的ARTIFACTS_DIR或STATE_DIR下,永远不要写到脚本旁边。Bun 模块加载不会在源码旁新增文件,Archon 也在工作流执行与可执行 fixture 中禁用了 Python 字节码缓存——这防止导入缓存改变冻结源码,但并不阻止你的脚本显式写文件。
冻结源码完整性
当一次运行使用捕获(captured)源码时,Archon 会在每次命名脚本尝试(含重试)之前、以及查找与子进程分发之前,将完整快照与运行固定的摘要及源码解析设置重新比对。任何变更都会在节点启动前拒绝它。内联脚本已包含在工作流定义中,执行时不读取快照。
这是检查点检测,不是密封或沙箱。以 Archon 用户身份运行的进程可以在检查后更改源码,已启动的并行节点也不会被取消。
扩展名与运行时映射
命名脚本的运行时由文件扩展名推导:
| 扩展名 | 运行时 |
|---|---|
.ts,.js | bun |
.py | uv |
节点上声明的runtime:必须与文件扩展名匹配——验证器会拒绝runtime: uv指向.ts文件,反之亦然。内联脚本则可以使用所选运行时支持的任何语言。该映射在源码中定义于 packages/workflows/src/script-discovery.ts(EXTENSION_RUNTIME_MAP),并在执行前由scriptDef.runtime作为唯一事实来源(dag-executor.ts)。
依赖管理(仅 uv)
deps直接透传给uv run --with <dep>,把包安装进每次运行独立的临时环境:
- id: scrape script: | import httpx r = httpx.get("https://api.github.com/repos/anthropics/anthropic-cookbook") print(r.text) runtime: uv deps: ["httpx>=0.27"]- 版本固定——任何 PEP 508 说明符都可用(
pkg==1.2.3、pkg>=2,<3)。 - bun 忽略
deps——Bun 在首次运行时自动安装导入的包,因此验证器会在runtime: bun配合deps时发出警告。要么删除该字段,要么在需要显式依赖管理时改用uv。 - 无持久环境——每次运行相互隔离,不需要维护
requirements.txt或 lockfile。
命令构建中deps的展开逻辑可在 dag-executor.ts 看到:nodeDeps.flatMap(dep => ['--with', dep])生成uv run --with dep1 --with dep2 ...;bun 内联分支则不加任何依赖标志。该行为有专门的测试覆盖(packages/workflows/src/script-node-deps.test.ts),断言 bun 内联带deps时仍只得到['--no-env-file', '-e', node.script]。
输出与数据流
stdout(去除尾部换行)成为$nodeId.output。若希望下游节点用$nodeId.output.field访问结构化字段,请打印 JSON——工作流引擎在when:条件和提示词替换中会尝试把输出解析为 JSON 以便字段访问。
当你希望这份 JSON 是契约而非约定时,声明output_format(见下节)。
- id: classify script: | const input = process.argv.slice(2).join(' '); const severity = input.includes('crash') ? 'high' : 'low'; console.log(JSON.stringify({ severity, length: input.length })); runtime: bun - id: investigate command: investigate-bug depends_on: [classify] when: "$classify.output.severity == 'high'"声明结果契约(Declaring a result contract)
没有output_format时,上面的$classify.output.severity靠约定工作:引擎解析文本并“期望”键存在。声明output_format后,同样的结果就变成节点拥有的契约:
- id: classify script: | const input = process.env.ARGUMENTS ?? ''; console.log(JSON.stringify({ severity: input.includes('crash') ? 'high' : 'low', units: [], })); runtime: bun output_format: type: object properties: severity: { type: string, enum: [low, high] } units: { type: array, items: { type: object } } required: [severity, units]声明 schema 后,节点会:
- 把 stdout 解析为一个严格的 JSON 文档——无代码围栏、无散文前言、无修复过程、无第二次尝试;
- 对照 schema 校验,不匹配即节点失败,并指出违规的 JSON 路径、引用 stdout 开头;
- 将规范化后的 JSON 文档发布为
$classify.output(下游绑定与fan_out.items的逻辑值),并把声明的属性名暴露给$classify.output.<field>; - 对未声明字段的引用会让消费节点失败,而不是静默解析为
''。
这与 AI 节点output_format所承载的契约完全一致,因此脚本节点与 Agent 在returns:节点后面可以互换。这对调用方(通过include:别名、workflow:子运行、扇出或工件指针)意味着什么,统一在工作流编写指南 → 结果契约中说明。
无 schema 的脚本行为不变:stdout 保持原始文本,仅像以前一样去除尾部换行。
该契约在源码中由certifyExecOutput实现(dag-executor.ts):stdout 不是严格 JSON 或不符合 schema 时抛出ExecOutputContractError,且该契约失败不进入子进程错误分类,因此不会触发重试(节点以retryable: false结束,见 dag-executor.ts)。
脚本中的变量替换
变量以原始字符串、不做 shell 引号包裹的方式替换进script文本——这与bash:节点不同(后者$nodeId.output的值会被自动加引号)。请把替换进来的值视为不可信输入,用语言特性去解析,而不要插值进 shell 语法。
:::caution[避免对$nodeId.output使用 String.raw]String.raw`$nodeId.output`看起来安全,但当替换值包含反引号时会静默失败——这在 AI 生成的 markdown、output_format载荷或任何含内联代码片段的输出中很常见。反引号会提前终止模板字面量,产生运行时的神秘Expected ";"解析错误。
请改用直接赋值。JSON 是 JavaScript 表达式语法的严格子集,因此替换值永远是合法的 JS 字面量:
// 安全——适用于任何合法 JSON,包括含反引号的内容 const data = $fetch-issue.output; // 脆弱——输出含反引号时会出错 const data = JSON.parse(String.raw`$fetch-issue.output`); // 不要这样写:::
对命名脚本,变量不会自动传入。请从环境变量读取(process.env.USER_MESSAGE、os.environ['USER_MESSAGE'])或通过 stdin 接收。对内联脚本,替换后的变量会在执行时直接嵌入代码字符串。
从源码看,脚本子进程的环境由 packages/workflows/src/exec-environment.ts 的buildExecNodeEnvironment构造,包含ARTIFACTS_DIR、STATE_DIR、LOG_DIR、ADOPTED_RUN_DIR、WORKFLOW_ID、BASE_BRANCH、USER_MESSAGE、ARGUMENTS、LOOP_USER_INPUT、LOOP_PREV_OUTPUT、REJECTION_REASON、CONTEXT等键。配置的项目级环境变量会先展开(dag-executor.ts),引擎保留键永远优先,防止 codebase 环境变量(如命名为ARGUMENTS)遮蔽传递通道。
环境与隔离
:::note[防止 shell 注入] 用户可控的工作流变量(如$USER_MESSAGE和$ARGUMENTS)通过环境变量传递给bash:节点(而非内联替换进 shell 命令),以阻止 shell 注入。脚本节点以同样的方式通过process.env接收这些值。 :::
脚本子进程接收process.env与你在 Web UI(设置 → 项目 → 环境变量)或.archon/config.yaml的env:块中配置的 codebase 级环境变量的合并结果。这与 Claude、Codex 和 bash 节点使用的注入面相同。
目标仓库.env隔离:Bun 子进程以--no-env-file调用,因此目标仓库.env中的变量不会泄漏进脚本。Archon 管理的环境(来自~/.archon/.env和<repo>/.archon/.env)正常透传。uv启动的 Python 子进程根本不会自动加载.env。完整机制见安全模型 → 目标仓库 env 隔离。
执行器还在提交前对工作流做静态输入检查:validateInlineExecInputs会扫描内联脚本中读取的环境变量(bun 匹配process.env.X,uv 匹配os.environ['X']),对未由绑定、声明输入或引擎提供的读取发出错误或警告,并精确到行号(packages/workflows/src/exec-input-validation.ts)。
校验(Validation)
archon validate workflows <name>会检查脚本节点:
- 脚本文件存在——命名脚本的基本名必须存在于所属工作流的
scripts/目录或旧式共享搜索路径中,且扩展名与声明的运行时匹配。文件缺失会校验失败,并给出期望路径的提示。 - 运行时在 PATH 上——
bun或uv必须已安装。缺失的运行时发出警告并附官方安装命令:curl -fsSL https://bun.sh/install | bashcurl -LsSf https://astral.sh/uv/install.sh | sh
deps搭配runtime: bun——警告deps在 Bun 下是空操作。
运行时可用性按进程缓存——检查只执行一次which bun/which uv并记忆结果。
实战模式(Patterns)
在下一个节点前转换 AI 输出
用脚本节点作为两个 AI 节点之间的确定性适配器:解析上游分类器的 JSON、过滤、转发干净的载荷:
- id: classify prompt: "Classify: $ARGUMENTS" allowed_tools: [] output_format: type: object properties: items: type: array items: { type: object } - id: filter script: | const upstream = JSON.parse(process.env.UPSTREAM ?? '{}'); const high = (upstream.items ?? []).filter(i => i.severity === 'high'); console.log(JSON.stringify(high)); runtime: bun depends_on: [classify] - id: triage command: triage-high-severity depends_on: [filter] when: "$filter.output != '[]'"(注:要真正填充UPSTREAM,需要把$classify.output内联替换进脚本正文。上面的示例用于说明结构。)
在~/.archon/scripts/放一个可复用助手
希望每个仓库都可用的助手——比如一个 triage 摘要格式化器——放在~/.archon/scripts/triage-fmt.ts:
// ~/.archon/scripts/triage-fmt.ts const raw = process.argv.slice(2).join(' ') || '{}'; const data = JSON.parse(raw); const lines = data.issues?.map((i: { id: string; title: string }) => `- [${i.id}] ${i.title}` ).join('\n') ?? ''; console.log(lines || 'no issues');然后在任何仓库的工作流中按名引用:
- id: format script: triage-fmt runtime: bun depends_on: [gather]Python 科学计算依赖
- id: analyze script: | import json, sys import pandas as pd data = json.loads(sys.argv[1]) if len(sys.argv) > 1 else [] df = pd.DataFrame(data) print(df.describe().to_json()) runtime: uv deps: ["pandas>=2.0"] depends_on: [collect]脚本节点不做什么(What Does NOT Work)
- AI 专属功能——
hooks、mcp、skills、allowed_tools、denied_tools、agents、model、provider、effort、maxBudgetUsd、systemPrompt、fallbackModel、betas、sandbox全部在运行时忽略,加载器会发出列出被忽略字段的警告。(output_format不在此列——脚本拥有它,见声明结果契约。) - JSON 修复与重试——认证脚本的 stdout 第一次就必须完全正确。没有围栏剥离、没有第二次尝试:不符合声明 schema 的 stdout 是脚本自身的 bug。
- 交互式提示——脚本以无头方式运行;任何
stdin读取会立即遇到 EOF。 bun与uv之外的运行时——解析阶段即被拒绝。- 执行中途取消——工作流取消时脚本子进程会被杀死,但没有协作式取消信号。请把脚本设计成快速完成或快速失败。
延伸阅读
- 工作流编写指南——完整工作流参考(
bash:节点、returns:节点、结果契约、子进程证据) - 全局工作流、命令与脚本——
~/.archon/scripts/的 home 级作用域 - 安全模型 → 目标仓库 env 隔离——环境隔离细节
- 变量参考——替换规则全集
【免费下载链接】ArchonThe first open-source harness builder for AI coding. Make AI coding deterministic and repeatable.项目地址: https://gitcode.com/GitHub_Trending/archon3/Archon
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考