【免费下载链接】sandcastle
Orchestrate sandboxed coding agents in TypeScript with sandcastle.run()
导读
Sandcastle 是一个用 TypeScript 编排沙箱化编码 Agent 的库,blank模板是其最简起点:src/templates/blank/prompt.md提供了一份仅含Context、Task、Done三个章节的提示词骨架,配合 src/templates/blank/main.mts 即可驱动 Claude Code 在 Docker 等沙箱中完成一次编码任务。读完本文,你将掌握如何基于这份骨架写出高可用的 Agent 提示词,理解!command`` 动态上下文注入与<promise>COMPLETE</promise>提前终止信号背后的源码级机制,并学会用{{KEY}}参数占位符把提示词模板化、可复用化。
一、blank 模板是什么:最小可用的提示词骨架
blank模板在init脚手架中描述为"Bare scaffold — write your own prompt and orchestration"(见 src/templates/blank/template.json),即"裸脚手架,提示词与编排逻辑全部由你自行填写"。它生成的项目里最重要的两个文件是:
.sandcastle/prompt.md—— 来自 src/templates/blank/prompt.md,定义 Agent 该做什么;.sandcastle/main.mts—— 来自 src/templates/blank/main.mts,用 JS API 控制 Agent 如何运行。
prompt.md全文如下,总共只有三个章节:
# Context <!-- Use !`command` to pull in dynamic context. Commands run inside the sandbox. --> <!-- Example: !`git log --oneline -10` or !`{{LIST_TASKS_COMMAND}}` --> # Task <!-- Describe what the agent should do. --> # Done <!-- When the task is complete, output <promise>COMPLETE</promise> to signal early termination. -->这个骨架并不是一个可"直接用"的提示词,而是一份结构化模板:它把一段生产级 Agent 提示词应当具备的信息组织方式固化下来——先给上下文(Context),再下任务(Task),最后定义完成判据(Done)。注释中已经点出两个核心机制,后面两节分别展开:
!command``:在沙箱内执行命令,把输出注入提示词,作为动态上下文;<promise>COMPLETE</promise>:Agent 输出该信号即可提前终止迭代循环。
值得注意的是,这份骨架并非只存在于模板目录。init命令使用的SKELETON_PROMPT常量(见 src/templates.ts)与prompt.md内容完全一致,并额外给出了!gh issue list --label Sandcastle --json number,title`` 的真实示例。也就是说,无论你通过模板文件还是通过脚手架常量拿到提示词,得到的是同一套三章节约定。
二、Context 章节:用!command`` 注入沙箱内动态上下文
# Context <!-- Use !`command` to pull in dynamic context. Commands run inside the sandbox. -->!command`` 是 Sandcastle 的shell 表达式(shell expression)语法:一个前导!加反引号包裹的命令。提示词经过预处理时,该表达式的标准输出会被原地替换进提示词,再交给 Agent。关键约束写在注释里:命令在沙箱内运行,因此你可以放心地读取工作树里的 git 历史、文件状态等"运行时才存在"的信息。
常见用法
# Context 仓库最近的提交历史: !`git log --oneline -10` 当前分支: !`git rev-parse --abbrev-ref HEAD`执行时,上述两行会被替换为命令的实际输出(如最近的 10 条提交、分支名),Agent 拿到的是"最新鲜"的上下文,而非写死的内容。这正是blank模板注释里!git log --oneline -10`` 示例的用意。
底层实现:Preprocessor 的并行执行与超时保护
!command`` 展开由 src/PromptPreprocessor.ts 的preprocessPrompt完成,其行为可以从源码确认:
- 并行执行:提示词中所有 shell 表达式通过
Effect.all(..., { concurrency: "unbounded" })一次性并发执行,多个命令互不等待(src/PromptPreprocessor.ts); - 30 秒硬超时:每个命令套用
Effect.timeoutOption(Duration.millis(30_000)),超时抛PromptExpansionTimeoutError,避免某个挂起命令卡死整个 run(src/PromptPreprocessor.ts); - 非零退出码即失败:命令
exitCode !== 0时抛PromptError,报错信息包含退出码与 stderr,遵循"快速失败"原则(ADR 0020,见 docs/adr/0020-prompt-expansion-fails-fast.md); - Token 预算可见:每个命令展开后按
stdout.length / 4估算 token 数并打印command → ~N tokens日志(src/PromptPreprocessor.ts),让你能判断某个上下文注入是否过度消耗模型输入; - 仅替换原始模板中的表达式:预处理只执行"被打标记"的块——源码通过
SHELL_BLOCK_MARKER(\x01)区分原始模板中的!command`` 与参数替换后混入的文本,后者一律当作纯数据,防止注入(src/PromptArgumentSubstitution.ts)。
一条重要限制:提示词必须来自promptFile才会执行 shell 表达式展开。若使用内联prompt: "...",提示词会原样交付给 Agent,不做任何替换与展开(见 src/PromptResolver.ts 中source: "inline" | "template"的区分)。
三、Task 章节:把"该做什么"写清楚
# Task <!-- Describe what the agent should do. -->骨架只留了一行占位注释,但这一章是整个提示词的实际工作量所在。基于骨架并参考仓库内其他成熟模板(如 src/templates/parallel-planner/implement-prompt.md)的写法,一个可运行的 Task 章节通常包含:
# Task 1. 阅读 Context 中提供的提交历史,确定本次改动范围。 2. 实现 XXX 功能,遵循仓库现有代码风格。 3. 为新增逻辑补充测试,并运行 `npm test` 确认全部通过。 4. 不要修改与本任务无关的文件。写 Task 时的实操建议:
- 拆成可核查的步骤:每一条都是 Agent 可以自检、你也可以在 Done 阶段验证的原子动作;
- 明确约束与禁区:如"不修改无关文件""不要运行破坏性命令",Agent 会把这些当作硬性要求;
- 给出验收方式:测试命令、lint 命令等可执行判据,比含糊的"保证质量"更有效。
四、Done 章节:<promise>COMPLETE</promise>提前终止机制
# Done <!-- When the task is complete, output <promise>COMPLETE</promise> to signal early termination. -->Done 章节约定的是完成信号:当任务完成时,Agent 在输出中写入<promise>COMPLETE</promise>,Sandcastle 检测到该子串后提前结束迭代循环,不必等到maxIterations跑满。这份骨架把"何时算完成"显式写进提示词,是避免 Agent 空转的关键。
从源码可以确认它的默认值与行为:
- 默认信号:
const DEFAULT_COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";(src/Orchestrator.ts); - 可自定义:
run()的completionSignal选项接受字符串或字符串数组,匹配方式为对 Agent 输出做includes子串判断(src/run.ts),因此你完全可以定义自己的信号,如completionSignal: ["<promise>DONE</promise>", "<promise>FAILED</promise>"]; - 完成宽限期:观察到信号后,默认再等 60 秒让 Agent 进程自然退出(期间仍继续捕获 token 用量、
result事件等尾部输出),若因gh/git 子进程或长驻 MCP 服务器占用 stdout 而迟迟不退,Sandcastle 会强制完成本轮并给出警告(src/run.ts,对应 ADR 0019,见 docs/adr/0019-completion-timeout-for-hanging-process.md); - 空闲超时兜底:即使信号缺失,Agent 连续 600 秒(默认
idleTimeoutSeconds)无输出也会被判为失败,双层防护保证编排不会无限挂起(src/Orchestrator.ts)。
这条机制在测试中被反复验证,例如Orchestrator.test.ts中"All done. <promise>COMPLETE</promise>"的输出即触发提前终止并断言result.completionSignal === "<promise>COMPLETE</promise>"(src/Orchestrator.test.ts);InitService.test.ts还验证了脚手架生成的提示词必然包含该信号(src/InitService.test.ts)。
五、把骨架模板化:{{KEY}}参数替换
blank模板的骨架刻意保持为空,但你完全可以像仓库其他模板那样,用{{KEY}}占位符把提示词参数化,实现"一份提示词、多次运行"。
占位符替换由 src/PromptArgumentSubstitution.ts 的substitutePromptArgs实现,规则如下:
- 占位符语法:
{{KEY}},正则/\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g(src/PromptArgumentSubstitution.ts); - 参数来源:
run()的promptArgs: PromptArgs,值为string | number | boolean(src/PromptArgumentSubstitution.ts); - 缺失即失败:提示词引用了占位符但
promptArgs未提供对应值(或值为 null/undefined),直接抛PromptError,避免 Agent 拿到未替换的模板(src/PromptArgumentSubstitution.ts); - 多余参数告警:提供了参数但提示词未引用,会输出 warning 提示(src/PromptArgumentSubstitution.ts);
- 内置参数不可覆盖:
SOURCE_BRANCH与TARGET_BRANCH由 Sandcastle 自动注入,试图通过promptArgs覆盖会报错(src/PromptArgumentSubstitution.ts); - 仅支持 promptFile:与 shell 表达式一致,
{{KEY}}替换同样只在promptFile模式下生效;内联prompt会原样传给 Agent,validateNoArgsWithInlinePrompt会直接拒绝同时传入promptArgs的做法(src/PromptArgumentSubstitution.ts)。
结合模板参数的示例
把骨架升级为可复用模板:
# Context 分支 {{SOURCE_BRANCH}} 上的最近提交: !`git log --oneline -10` # Task 修复 issue #{{ISSUE_NUMBER}} 描述的问题,并补充回归测试。 # Done 完成时输出 <promise>COMPLETE</promise>。对应编排代码:
import { run, claudeCode } from "@ai-hero/sandcastle"; import { docker } from "@ai-hero/sandcastle/sandboxes/docker"; await run({ agent: claudeCode("claude-opus-4-8"), sandbox: docker(), promptFile: "./.sandcastle/prompt.md", promptArgs: { ISSUE_NUMBER: 42 }, });注意!git log ...`` 与{{SOURCE_BRANCH}}可以共存:参数先被替换,随后 shell 表达式在沙箱内展开,最终提示词里不存在任何占位符。
六、从骨架到首次运行:init 脚手架与编排入口
blank模板不是孤立存在的,它的上下游配套如下:
生成:运行
npx @ai-hero/sandcastle init,选择blank模板后,脚手架把 src/templates/blank/prompt.md 与 src/templates/blank/main.mts 复制到项目的.sandcastle/目录(复制逻辑见 src/InitService.ts 附近的copyTemplateFiles)。定制:
init完成后的"Next steps"提示明确要求:"Read and customize .sandcastle/prompt.md to describe what you want the agent to do"(src/InitService.ts),随后定制main.mts,并把"sandcastle": "npx tsx .sandcastle/main.mts"写进package.jsonscripts(src/InitService.ts)。编排入口:模板自带的 src/templates/blank/main.mts 是最小编排示例:
import { run, claudeCode } from "@ai-hero/sandcastle"; import { docker } from "@ai-hero/sandcastle/sandboxes/docker"; // Blank template: customize this to build your own orchestration. // Run this with: npx tsx .sandcastle/main.mts // Or add to package.json scripts: "sandcastle": "npx tsx .sandcastle/main.mts" await run({ agent: claudeCode("claude-opus-4-8"), sandbox: docker(), promptFile: "./.sandcastle/prompt.md", });- 运行:
npm run sandcastle(或npx tsx .sandcastle/main.mts)。
在这个入口里,promptFile指向的就是你定制过的prompt.md。run()会先经 src/PromptResolver.ts 读取文件并标记为template源,随后依次完成{{KEY}}参数替换与!command`` 展开,最终把成品提示词交给沙箱内的 Agent(展开位置见 src/Orchestrator.ts)。Sandcastle 随后负责沙箱生命周期、git 分支策略与提交回合并,你只需关注提示词本身。
七、对照其他模板:骨架的可扩展方向
仓库内其他模板展示了这份三章节骨架的进阶形态,可作定制参考:
- src/templates/simple-loop/prompt.md:在骨架基础上加入
!{{LIST_TASKS_COMMAND}}`` 动态任务列表、git 历史注入与明确的完成条件,是"单 Agent 简单循环"的标准写法; - src/templates/parallel-planner/implement-prompt.md:同一骨架在多 Agent 流水线(planner → implementer → merger)中复用,
!git log -n 10 --format=...`` 注入结构化提交历史; - src/templates/sequential-reviewer/implement-prompt.md:展示 review 场景下
!git diff {{TARGET_BRANCH}}...{{BRANCH}}`` 的 diff 上下文注入。
它们的共同规律是:Context 章节尽量注入运行时的真实数据,Task 章节明确步骤与约束,Done 章节统一用<promise>COMPLETE</promise>收尾。掌握了 blank 骨架,你就掌握了 Sandcastle 全部内置模板的提示词组织范式,可以在此基础上自由组合!command``、{{KEY}}与自定义completionSignal,构建属于自己的多 Agent 编排流水线。
小结
blank模板的prompt.md是 Sandcastle 提示词工程的最小单元:Context章节负责用!command`` 在沙箱内注入动态上下文(并行执行、30 秒超时、非零退出码快速失败),Task章节承载实际任务描述,Done章节通过<promise>COMPLETE</promise>实现提前终止(默认信号、60 秒宽限期、600 秒空闲兜底)。在此基础上叠加{{KEY}}参数替换,即可把单次提示词升级为可复用的模板。从npx @ai-hero/sandcastle init到npm run sandcastle,整条链路在 src/InitService.ts、src/PromptResolver.ts、src/PromptPreprocessor.ts、src/Orchestrator.ts 中均有清晰实现可循,是一套"从空骨架到自定义沙箱 Agent"的完整、可验证的实践路径。
【免费下载链接】sandcastle
Orchestrate sandboxed coding agents in TypeScript with sandcastle.run()
相关推荐
LMCache源码解析:KV缓存层如何让长上下文推理跳过Prefill(完整指南)
LMCache源码解析:KV缓存层如何让长上下文推理跳过Prefill(完整指南) LMCache 是一个面向 LLM 推理的 KV 缓存层 :它把推理引擎(如
Quivr提示工程:自定义提示词模板和优化技巧
Quivr提示工程:自定义提示词模板和优化技巧 引言:为什么提示工程对AI助手至关重要 在人工智能助手领域,提示工程(Prompt Engineering)是决
人工智能AI 应用大模型RAG后端前端baoyu-infographic 提示词模板工程:从 base-prompt.md 到生产级信息图生成
baoyu infographic 提示词模板工程:从 base prompt.md 到生产级信息图生成 导读 base prompt.md 是 baoyu i
AI 技能AI 插件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考