247-agent 模型动作
守护进程的设计规则:控制流中不包含 LLM。匹配、去重、路由、重试和发布均由代码完成。模型仅能在 decide、llm 或 agent 动作内部运行,使用能完成任务的最便宜层级,并受预算限制。本技能旨在指导如何正确编写这三类动作及其相应的执行器。
首先判断是否需要模型
按以下顺序询问:
- 触发器的
filter能决定吗?(发送者、标签、仓库、关键字)如果是,则不需要模型调用。 - 脚本或连接器操作能确定性地产出答案吗?如果是,则使用
shell/connector。 - 是标签、是/否或等级判断,且无需生成文本(分类、路由、分诊、门控)?则使用
decide:一个纯分类模型,校准后的概率,成本约为llm调用的百分之一。 - 是一次具有固定输出形状且需要文本的判断(提取字段、总结、起草)?则使用
llm,首选 Haiku 模型。 - 是否需要读取文件、运行命令并迭代?则使用
agent,限定在能成功完成的最窄范围内。
两个智能需求不同的任务应为相同动作种类,但配置不同的 model/effort/max_tool_calls/system_file,而不是不同的代码。参考 docs/examples/website-updates.yaml 中的任务 2-4 了解标准格式。
模型 ID 和参数
| id | 用途 | 备注 |
|---|---|---|
claude-haiku-4-5 | 分类、提取、简短摘要 | 无 effort 参数 |
claude-sonnet-5 | 限定范围的代理、较难的提取 | 支持 effort 和自适应思考 |
claude-opus-5 | 广范围的代理、困难的判断 | 支持 effort 和自适应思考 |
模型通过命名的提供者访问(providers: 在 agent.yaml 中,provider: 在动作上;anthropic、openai 或 openrouter)。其他提供者的 ID 原样使用,除非内置表格已知(目前为 OpenAI 模型)或提供者报告成本(OpenRouter),否则需要 pricing: 条目。
ID 无日期后缀。无 assistant prefill。在构建模型级联之前,先在相同输入上以 effort: low 测量更强模型:当前代模型,以低 effort 运行时往往胜过以高 effort 运行的较弱模型,且单一模型意味着单一提示缓存命名空间。
编写 decide 任务
完整字段列表、连线说明和陷阱见 references/decide-action.md。要点:
action:
kind: decide
provider: openrouter # 必须是 openrouter 提供者;默认使用 defaults.decide.provider
budget: { max_usd: 0.001 }
state: # 被判断的内容;字符串可模板化,键名不可
subject: ${event.payload.subject}
body: ${event.payload.body}
questions: # 静态策略;此处无 ${…}
kind:
type: choice
instructions: 发送者想对网站做什么?
criteria:
event_list_update: 添加、删除或修改事件列表中的条目
general_change: 对网站的任何其他更改
ignore: 不是更改请求(问题、感谢、垃圾信息)
urgent:
type: noul
instructions: 发送者是否需要在今天内完成?
- 结果是答案映射:
result.kind.choice、result.kind.confidence、result.urgent.noul(P(true))、result.<score>.score(等级索引的概率加权均值)。使用emit … when:进行路由;阈值位于任务中,绝不在执行器中,且需基于真实数据校准。 - 始终包含一个回退标签(
ignore、other):缺少它,离题输入会得到自信的错误答案。 - 标准需严格遵循:它们是策略,像代码一样进行版本控制。保持选项顺序稳定(顺序变化会影响置信度)。在一次调用中提出多个问题;每个额外问题只消耗其自身的 token。
- 输入内容是数据:它放在
state中,绝不放在问题中。整个请求不得超过 32k token。 - 只有
openrouter提供者类型能访问 Jev(typesafe/jev-1.13,默认模型);oa validate会拒绝其他任何类型。无需pricing:条目。
编写 llm 任务
完整字段列表和执行器说明见 references/llm-action.md。要点:
action:
kind: llm
model: claude-haiku-4-5
max_tokens: 512
system_file: prompts/classify_email.md # 静态 → 提示缓存
input: |
Subject: ${event.payload.subject}
<email>
${event.payload.body}
</email>
output_schema:
type: object
additionalProperties: false
required: [kind, summary]
properties:
kind: { enum: [event_list_update, general_change, ignore] }
summary: { type: string }
- 将系统提示保存在文件中,位于
prompts/下,静态且先渲染,以便跨运行缓存。将易变的事件数据放在最后,即input中。 - 输入内容(电子邮件、聊天、PR 文本)不可信。将其包裹在分隔块中(
<email>…</email>),并在系统提示中告知模型此为数据。 - 始终在模式中包含一个逃生口(
ignore、unknown、confidence),以便路由emit.when可以丢弃不可操作的结果而无需再次调用。 - 对结果使用
emit … when:进行路由;下游任务在此事件上触发。 - 对于可以等待的任务使用
batch: true(半价,结果以事件到达)。
编写 agent 任务
完整字段列表和执行器说明见 references/agent-action.md。要点:
action:
kind: agent
connector: claude # 一种传输方式:acp 连接器(connectors.d/claude.yaml;sandbox: bwrap 在此)
model: claude-sonnet-5 # 可选:在会话上设置(ACP 配置选项);未设置则使用代理默认值
effort: medium # 可选:代理的 thought_level 选项
max_tool_calls: 20
budget: { max_usd: 0.50 }
workspace: { kind: git-worktree, repo: /var/lib/247-agent/repos/site, branch: main }
tools: [read, edit, search, execute] # ACP 工具种类;其他一切均被拒绝
bash_allow: ["npm run build"] # 代理运行的任何命令必须匹配
system_file: prompts/agent_event_list.md
prompt: |
根据以下请求更新 data/events.yaml。不要触碰其他文件。
完成后运行 `npm run build`。
<email>
${event.payload.email.body}
</email>
result: { path: RESULT.json, schema: schemas/site_change.json }
post: # 仅当 status: done 时
- shell: ["npm", "run", "build"]
- shell: ["git", "commit", "-am", "events: ${result.summary}"]
emit:
- type: site.change_done # 在此事件上路由审批,绝不在 task.<name>.succeeded 上
when: "result.status == 'done'"
payload: { summary: "${result.summary}" }
- type: site.change_blocked # 代理无法完成:说明缺少什么
when: "result.status == 'blocked'"
payload: { summary: "${result.summary}", missing: "${result.missing}", email: "${event.payload.email}" }
以下非协商项目,是使代理安全无人值守运行的关键:
- 每次运行使用全新工作空间(
git-worktree在分支agent/<run_id>上,或temp),失败时删除,成功时保留。代理从不编辑基础检出。 tools+bash_allow是全部能力范围,按权限请求判断,绝不授予“始终允许”;代理未询问即进行的工具调用将在事后判断,违反规则将导致运行失败(unasked_execute: sandboxed仅放宽命令检查,且仅针对 Codex,其自身的沙箱限制了它未询问的内容)。授予最小权限;提示词不是安全边界。mcp_servers将连接器操作作为工具添加:授予命名的操作({ connector: email, ops: [fetch_new] })而不是整个连接器,为其代理种类(对于 claude-agent-acp 为other)添加到tools,并且绝不使用能自行发布或发送的连接器。- 代理绝不持有部署密钥,也绝不发布。 它进行编辑,
post门验证构建仍然可用,提交记录更改,由单独的shell任务发布。回滚是git revert加重新发布。模型密钥存在于连接器清单的env中,而不是任务中。 RESULT.json是契约,执行器在每个提示中声明它:status: done | blocked加上summary,然后是任务的result.schema。done运行门控;blocked跳过门控且运行仍然成功,因此emit … when: "result.status == 'blocked'"将“未完成因为缺少 X”路由到回复任务。设计每个代理任务时都应包含两个分支,并在done事件上触发发布。post门是确定性的,无模型参与。门控失败导致运行失败。max_tool_calls和budget是硬性停止点(会话被取消);代理未报告用量或成本的一次轮次将导致运行失败。- 共享同一仓库的任务设置
concurrency: 1;timeout是每次尝试的挂钟时间。 - 高影响更改在发布前需要一个审批门(
wait+ 聊天);247-agent-tasks技能中有此模式。
预算与账本
每个模型调用都通过 ctx.llm 端口进行,该端口将用量(提供者、模型、输入/输出/缓存 token、USD)记录在账本中,并强制执行运行的 budget.max_usd(调用前最坏情况,调用后实际情况:超支导致运行失败)和全局 budgets.daily_usd 每个 UTC 日(一旦超过,当日所有模型调用快速失败,并发出一次 budget.exceeded 事件,notify 任务应监听它)。不含价格的模型无法通过验证。切勿添加未预算的调用,包括执行器内部的“辅助”调用:使用 ctx.llm,绝不直接使用 SDK。agent 轮次是唯一模型由他人(ACP 代理,使用自己的密钥)调用的情况:执行器将代理通过 ctx.llm.record 报告的成本以连接器名称记入账本,并在报告的成本超过上限时取消会话。oa cost --by task --since 7d 显示账本。
当前状态
llm 动作已验证,与 providers:/pricing: 交叉核对,可通过 ctx.llm 运行,应用账本和预算,且所有三种提供者类型(anthropic、openai、openrouter)均有适配器。batch: true 在 anthropic 提供者上工作(消息批量,运行挂起直到 llm.batch.ended;见 references/llm-action.md)。decide 动作通过同一端口(ctx.llm.decide())运行,Decisions API 仅在 openrouter 适配器中实现;docs/examples/decide-triage.yaml 是参考。agent 动作运行在 ACP 连接器上(transport: acp);docs/examples/website-updates.yaml 中的任务 3、3b 和 4 是参考,docs/examples/connectors.d/claude.yaml 是连接器;会话作为转录持久化(oa runs logs <id>),随运行由 retention.runs 删除。mcp_servers 通过核心的工具桥接器(packages/core/src/connectors/mcp-bridge.ts)向会话提供连接器操作。动作上的 model 和 effort 设置会话的 ACP 配置选项(类别 model 和 thought_level)。已完成运行的 work/<run_id> 由 retention.workspaces(默认 7 天)在运行结束后清理。
添加适配器或更改执行器时,遵循 references/llm-action.md、references/decide-action.md 和 references/agent-action.md,并保持 docs/internal/model-actions.md、docs/internal/agent-action.md 以及 docs/tasks/ 下的用户页面与代码同步。