Documentation
¶
Overview ¶
Package executor 定义 handoff 的 executor 挂载契约:Adapter 接口与事件/结果类型。
职责:
- 定义「五动作」Adapter 契约:Start / Events / Send / RespondPermission / Stop
- 定义 AdapterEvent(permission/question/progress/result 四类)与 Result 数据结构
- 为不同 executor(opencode、Claude Code、grok)预留统一挂载点,实现方只实现本契约
边界:
- 实现方(adapter)不得直接碰 store——所有任务状态与工单都由 manager 写入, 这条边界防止 adapter 与 manager 双写打架:状态机只有一个写入者(manager), adapter 只负责「执行、产事件、收指令」,任何持久化诉求(如记录会话 id) 都以事件或返回值形式交给 manager 落库
- 本包为纯契约:无 I/O、无实现、无外部依赖(仅引用 proto 的数据结构)
幂等约定:
- AdapterEvent.PermissionID 由实现方生成并保证稳定:同一权限请求(如 SSE 断线重连后的重放)携带相同 PermissionID。manager 把它按任务命名空间化为 ticket id(taskID:permID)——同任务重放时 CreateTicket 按 id 幂等,天然 去重;跨任务 permID 碰撞也不会互相吞工单。回传 executor 时还原裸 permID
oneshot.go:执行者名字 → 一次性 CLI argv 的唯一映射点。
职责:
- 把「执行者名 + 模型 + prompt」翻译为一次性命令行调用(prompt 作末位参数)
- 审批者的 CLI 裁决与未来的降级/补充调用共用这一个映射,保证各执行者的 调用形态只在一点登记,不会出现多处各自拼 argv 的漂移
边界:
- 不执行命令、不读配置、不做任何 I/O
- 新执行者(如 grok)在此登记:加一个 switch 分支即接入
probe.go —— 只读存活探测的共享数据契约。
职责:
- 定义 ProbeReq / ProbeOutcome,供 manager 与各 adapter 共用
边界:
- 只有数据,没有接口:能力接口由消费方(manager)定义并做类型断言,这样 「不支持探活的 adapter 一律按 unknown 处理」是自然语义,executor.Adapter 的五动作核心契约也不被污染(与 resume.go 同规格)
- 无 I/O、无实现
reconcile.go —— 断连窗口会话对账的共享数据契约(B38)。
职责:
- 定义 ReconcileOutcome,供 manager 与各 adapter 共用
边界:
- 只有数据,没有接口:与 resume.go 同规格——对账能力的接口由消费方 (manager)定义并做类型断言,这样「不支持对账的 adapter」仍是自然语义, executor.Adapter 的五动作核心契约也不被污染
- 无 I/O、无实现
resume.go —— 执行恢复的共享数据契约。
职责:
- 定义 ResumeReq / ResumeOutcome / ResumeMode 常量,供 manager 与三个 adapter 共用
边界:
- 只有数据,没有接口:恢复能力的接口由消费方(manager)定义并做类型断言, 这样「不支持恢复的 adapter 一律按不存活走 failed 恢复路径」仍是自然语义, executor.Adapter 的五动作核心契约也不被污染
- 无 I/O、无实现(与本包其余部分同规格)
Index ¶
Constants ¶
const ( // VoidReasonExecutorGone 用于 executor 真的没了:进程退出、连接终止、看门狗判死。 VoidReasonExecutorGone = "executor 已终结" // VoidReasonTurnDiscipline 用于 executor 活着但没守回合纪律:无 trailer、零文本。 VoidReasonTurnDiscipline = "回合未按纪律收尾,executor 仍在线" )
挂起工单被作废时写进审计事件的理由。
为什么要区分:作废行为本身在两种情形下相同(回合已结束,挂起工单无论如何 都该作废),但审计记录必须说真话。历史上零文本回合那条 FailReason 明写 「executor 仍在线,可 continue 续接重试」,却被同一次调用标记成 「executor 已终结」——审计与事实互相矛盾。
const ( PermToolBash = "bash" PermToolWrite = "write" PermToolEdit = "edit" PermToolWebFetch = "webfetch" PermToolOther = "other" // 未识别的工具:判据退回按描述全文处理 )
归一化工具名:各 adapter 的原始工具名折算到这一组常量,permgate 只认它们。
为什么要归一化:三个 executor 对同一件事的叫法不同(claude 的 "Write"、 opencode 的 "edit"、grok 的工具名见 Task 1 探针),而判据只有一份。
const ( ResumeModeReattach = "reattach" // 第 2 级:进程还活着,重连 ResumeModeCold = "cold" // 第 3 级:重起进程 + 载入原会话,上下文完整 ResumeModeFresh = "fresh" // 第 4 级:原会话载不进,已新开会话,上下文从下一条指令开始 )
恢复实际走到的级别(ResumeOutcome.Mode 的取值)。
四级阶梯的第 1 级「运行态还在 adapter 内存里」不在此列——那种情况根本不会 调到 Resume。
const TruncationMarker = "…(已截断)"
TruncationMarker 是文本截断的显式标记,追加在截断文本末尾(如权限描述里的 bash 命令超限时)。
契约:executor 侧截断时必须以本常量收尾(opencode 的 truncateMarked), manager 侧据此 fail-closed——权限文本含本标记说明「审核/裁决者看到的是截断 后的不完整命令」,危险片段可能落在截断之外,黑名单与廉价模型都不可信,必须 升级人工协调者。
注意(B6 起):权限描述在 executor 侧只受 64KB 防失控硬上限约束,常规长度 不再触发本标记——只有真的超了 64KB 才会出现它。事件 payload 的短展示由 manager 侧另行截断(同样以本常量收尾)。
Variables ¶
var ErrTaskNotRunning = errors.New("任务不在运行中")
ErrTaskNotRunning 表示任务在本 adapter 里没有运行态:executor 已终结、 从未启动,或运行态已随 Stop 注销。
实现方在 Send / RespondPermission / Stop 遇到这种情况时必须包装本哨兵错误 (fmt.Errorf("...: %w", executor.ErrTaskNotRunning)),调用方据此区分 「executor 已经不在」与「executor 还在但这次调用失败了」——两者的处置完全 不同:前者应把任务交协调者裁决,后者应保持可重试。上层禁止靠错误文本判别。
Functions ¶
func NormalizePermTool ¶
NormalizePermTool 把 executor 的原始工具名折算为归一化名。
参数:raw 为 executor 侧的原始工具名,允许带空白、大小写任意 返回:PermTool* 之一;未识别时返回 PermToolOther
注意:本表只收本项目**实测见过**的名字。新增 executor 时先取真实样本再 补表,不要按想象加别名——猜错的代价是判据静默走错路由。
func OneShotArgs ¶
OneShotArgs 返回执行者的一次性调用 argv(prompt 作为末位参数)。
参数:
- executorName: 执行者名,目前支持 opencode / claude / grok;未知名字返回错误
- model: 模型名;空表示让执行者用自身默认模型(省略对应参数)
- prompt: 一次性 prompt 原文,作为命令的最后一个参数
返回:
- 可直接交给 exec.CommandContext 的 argv;映射失败时返回错误, 错误文本列出支持的执行者名
Types ¶
type Adapter ¶
type Adapter interface {
// Start 异步启动任务执行并立即返回。
//
// 参数:
// - ctx: 控制启动阶段的超时/取消;不代表执行生命周期(执行延续到 Stop)
// - req: 任务快照、计划原文与任务工作目录
//
// 返回:
// - 启动失败(如 executor 不可达)时返回错误,此时任务应标记 failed
//
// 注意:
// - 启动后 executor 侧产生的事件经 Events 通道持续送达,与返回时机无关
Start(ctx context.Context, req StartReq) error
// Events 返回该任务的事件流通道(Start 后可用)。
//
// 注意:
// - 通道关闭表示执行终结(Stop 或内部异常退出),manager 据此结束中介循环
Events(taskID string) <-chan AdapterEvent
// Send 回答提问 / 回发修改指令,对同一会话续接执行。
//
// 参数:
// - taskID: 目标任务
// - text: 回答原文或续接指令,必须原样透传,不得加工
Send(ctx context.Context, taskID, text string) error
// RespondPermission 应答 executor 的权限请求。
//
// 参数:
// - taskID: 目标任务
// - permID: 权限请求 id(与事件中的 PermissionID 一致;manager 的 ticket id
// 经 taskID:permID 命名空间化,此处传裸 permID,不得传命名空间化 id)
// - decision: "once"(批准本次)或 "reject"(拒绝)
RespondPermission(ctx context.Context, taskID, permID, decision string) error
// Stop 终止任务执行并回收 executor 侧资源,事件通道随即关闭。
Stop(taskID string) error
}
Adapter 是 executor 挂载契约,实现方与 manager 的交互面就是这五个动作。
- Start: 异步启动执行,立即返回;事件通过 Events 通道持续产出
- Events: 该任务的事件流(Start 后可用),通道关闭表示执行终结
- Send: 回答提问 / 回发修改指令(同一会话续接,上下文完整保留)
- RespondPermission: 应答权限门,decision 取 "once"(批准本次)或 "reject"(拒绝)
- Stop: 终止执行并回收资源(事件通道随之关闭)
边界:
- 实现方不写 store、不做审批判断(见包级边界说明),「批不批」由 manager 根据协调者(人)的应答决定后经本接口回传
type AdapterEvent ¶
type AdapterEvent struct {
Type string // "permission" | "question" | "progress" | "result"
PermissionID string // Type=permission 时有效(manager 按其派生 ticket id,天然幂等)
// QuestionID 是 Type=question 时 executor 侧提问请求的**原生**稳定 id
// (如 opencode 的 que_xxx)。manager 按其派生 ticket id 使其幂等——
// 没有它,agentd 重启后 executor 重放同一个提问会产出第二张工单,
// 而旧工单永远答不掉(B58)。
//
// 留空表示该 executor 没有原生 id(claudecode/codex/grok 的 trailer ask
// 就是这种),manager 退回 uuid,行为与今天一致。
QuestionID string
SessionID string // 可选:executor 会话标识,manager 落 task.ExecutorSession;空则忽略
Text string // permission 描述 / question 原文 / progress 文本
// Perm 是 Type=permission 时的结构化载荷;nil 表示 adapter 提取不出结构,
// manager 据此 fail-closed 升级人工(看不懂的请求交给人)。
Perm *PermRequest
Result *Result // Type=result 时有效
}
AdapterEvent 是 executor 侧产出的单向事件,由 manager 中介循环消费。
Type 取值:"permission" | "question" | "progress" | "result",语义:
- permission: 权限门请求,PermissionID 有效(manager 按其派生命名空间化的 ticket id,见包级幂等约定),Text 为权限描述(如 "Bash: rm -rf node_modules"); 等待人工裁决
- question: 提问,Text 为问题原文;等待人工回答
- progress: 进度播报(可选心跳),Text 为进度文本;只入库不阻塞
- result: 回合终态,Result 有效;之后是否续接由协调者决定
SessionID 可携带于任意事件(含 progress):manager 收到非空 SessionID 时落 task.ExecutorSession(空则忽略,向后兼容)。progress 带它是「会话就绪」信号 ——审核主路径常以 question 收尾、result 永不出现,progress 是会话 id 到达 manager 的可靠通道;result 携带它是双保险(见 adapter 的会话就绪 emit)。
type PermRequest ¶
type PermRequest struct {
Tool string // 归一化工具名,取上面的 PermTool* 常量
Command string // Tool=bash 时的完整命令串(不截断)
Paths []string // Tool=write|edit 时的目标路径(可为相对路径)
}
PermRequest 是权限请求的结构化形态。
它与 AdapterEvent.Text 不是重复:Text 是给人看的全文(工单与展示的唯一 真相源),PermRequest 是给判据看的字段。拍平成字符串会丢掉工具名与路径, 那正是黑名单只能对整串做正则、于是既误判又漏判的根因。
边界:
- adapter 提取不出结构时**不要伪造**:整个 Perm 置 nil,manager 会 fail-closed 升级人工。填一个空壳会让判据误以为拿到了结构
type ProbeOutcome ¶
ProbeOutcome 是一次探测的结论。
字段说明:
- Alive: executor 是否仍在
- Note: 一句话理由,直接给协调者看(如「执行者进程 pid 1234 不存在」); Alive=true 时为空
三态怎么区分:实现方用 error 表达「探不出结论」——err != nil 即 unknown, 调用方**不得把它当 dead**。假阳性是诊断命令最贵的失败模式。
type ProbeReq ¶
ProbeReq 是一次只读存活探测请求。
字段说明:
- TaskID: 目标任务
- TaskDir: DataDir/tasks/<id>,恢复凭据(serve.json / claude.json)在里面
- SessionID: 落库的 task.ExecutorSession
type ReconcileOutcome ¶
ReconcileOutcome 是一次对账的结论,供 CLI 呈现与日志记录。
字段说明:
- TurnEnded: 断连期间回合是否已完结
- Emitted: 补发的终态事件数。取值只有 0 或 1——一个断连窗口内至多跨越 一个回合边界(新回合只能由经过 agentd 的 Start/Send 发起)
- Pending: 重新上报的悬而未决权限请求数。**opencode 恒为 0**:它的消息流 里 tool part 只有 callID 没有权限 id,应答端点要求真实 id、伪造即 404, 故建出的工单批了也送不回去——宁可不建
- Note: 一句话结论,直接给协调者看
type Result ¶
type Result struct {
Branch string // executor 工作分支名(如 handoff/T1)
CommitHash string // 回合收尾 commit 的哈希
SessionID string // executor 会话标识(如 opencode session id),供续接与归档
Summary string // 执行摘要(给协调者看的完成说明)
OK bool // true=正常完成;false=失败(见 FailReason)
FailReason string // OK=false 时的失败原因/日志尾部
// VoidReason 是本次失败导致挂起工单被作废时写进审计事件的理由。
// 空表示沿用缺省 VoidReasonExecutorGone——绝大多数失败路径(进程退出、
// 看门狗判死)确实是 executor 没了,不必逐个填。
VoidReason string
}
Result 是一次执行回合的终态结果(OK 或 FailReason 二选一)。
type ResumeOutcome ¶
ResumeOutcome 是恢复结果。
字段说明:
- Alive: 是否拿到了可用的运行态。false 时其余字段除 Note 外均为空
- Mode: ResumeMode* 之一;Alive=false 时为空串
- SessionID: 恢复后实际生效的会话 id。fresh 时是新 id,manager 据此落库
- Note: 一句话结论,manager 转成事件文本或错误信息给协调者看
type ResumeReq ¶
type ResumeReq struct {
TaskID string
TaskDir string
RepoPath string
SessionID string
Env []string
Model string
Cold bool
}
ResumeReq 是一次恢复请求。
字段说明:
- TaskDir: DataDir/tasks/<id>,恢复凭据(serve.json / claude.json)与会话 数据(grok 的 grokhome)都在里面
- RepoPath: 任务工作区(worktree 任务为 Workdir)。冷恢复时它是新进程的 cwd, claude 的会话文件路径按 cwd 编码,传错等于找不到会话
- SessionID: 落库的 task.ExecutorSession;空则无法载入原会话
- Env: 冷恢复重起进程时要注入的环境变量(KEY=VALUE,已解析已展开)。 **不传就是静默故障**:进程能起来,但用户配的代理/密钥全没了,一调模型才失败。 值绝不进日志,要打只打 key 名
- Model: 冷恢复重起进程时的模型名(serve.json/claude.json 都没存它, 只有 task.Model 有)
- Cold: true=允许冷恢复(进程已死时重起进程 + 载入原会话); false=只热重连,进程不在即判不可恢复
type StartReq ¶
StartReq 是 Adapter.Start 的入参:任务上下文 + 计划内容 + 任务工作目录。
字段说明:
- Task: 任务快照(ID/仓库/目标等),adapter 可据此写日志与恢复凭据
- PlanContent: 解码后的计划原文(dispatch 时从 plan_b64 还原),adapter 负责 把它加工成 executor 的启动 prompt
- TaskDir: 任务专属目录(agentd 在 DataDir/tasks/<id> 下创建),adapter 可把 配置、日志、渲染文件等任务物料写在这里
- Env: 启动 executor 进程时额外注入的环境变量(形如 KEY=VALUE,已解析已展开)。 由 manager 按 task.Executor 从 env 文件解析后填入;nil/空表示不注入。 实现方必须把它注入到自己拉起的进程环境中——这是 B19 对所有 adapter 的统一要求, 放在契约上而非各 adapter 的构造参数上,是为了让后续 adapter(Claude Code、grok) 不必各写一份注入逻辑
Directories
¶
| Path | Synopsis |
|---|---|
|
adapter.go —— Claude Code 语义到 executor.Adapter 契约的翻译层。
|
adapter.go —— Claude Code 语义到 executor.Adapter 契约的翻译层。 |
|
adapter.go —— codex 的 executor.Adapter 实现:五动作与事件翻译。
|
adapter.go —— codex 的 executor.Adapter 实现:五动作与事件翻译。 |
|
Package fake 提供脚本化的 executor.Adapter 实现,供集成测试驱动全链路。
|
Package fake 提供脚本化的 executor.Adapter 实现,供集成测试驱动全链路。 |
|
acp.go —— ACP(Agent Client Protocol)的 WebSocket JSON-RPC 双向客户端。
|
acp.go —— ACP(Agent Client Protocol)的 WebSocket JSON-RPC 双向客户端。 |
|
adapter.go —— opencode 语义到 executor.Adapter 契约的翻译层。
|
adapter.go —— opencode 语义到 executor.Adapter 契约的翻译层。 |
|
Package rawtap 提供上游原始字节的旁路归档。
|
Package rawtap 提供上游原始字节的旁路归档。 |
|
fallback.go —— 「回合结束但没有协议 trailer」时的共用裁决。
|
fallback.go —— 「回合结束但没有协议 trailer」时的共用裁决。 |