opencode

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

adapter.go —— opencode 语义到 executor.Adapter 契约的翻译层。

职责:

  • 把 StartServe/CreateSession/PromptAsync/SubscribeEvents 编排成 Adapter 的 五动作:Start(环境物料 → serve → 会话 → 初始 prompt → 订阅映射)、 Send(同一会话续接)、RespondPermission(权限应答转发)、 Stop(kill serve + 关事件流)、Events(事件通道)
  • SSE 事件 → AdapterEvent 映射:permission.asked → permission 事件;模型 文本(message.part.updated / message.part.delta)增量累积 → render.log 追加 + 节流 progress;session.status idle → turn.ParseTrailer 分类(ask/finish/none 兜底 git 实况裁决);serve 死亡 → failed result
  • 可见性:回合文本增量追加到 <taskDir>/render.log,供 handoff attach (render 流式 endpoint)旁观模型执行

边界:

  • 不写 store、不做审批判断(见 executor.go 包级边界):会话 id 等一切持久化 诉求经事件(progress「会话就绪」/ Result.SessionID)或返回值交给 manager 落库
  • 不做任务状态机迁移:6 状态迁移完全由 manager 负责,本层只产事件、收指令
  • 不重试、不决策:SSE 解析宽容(未知事件 Debug 跳过、绝不 panic); trailer 缺失时兜底只做「是否有新提交」的事实裁决,没有新提交就交协调者

事件映射以真实 SSE 样本为准(spike3/spike5,opencode 1.18.15 serve 模式):

  • 文本载体:模型文本走 message.part.updated(properties.part.type=text, 带该 part 全量文本快照)与 message.part.delta(properties.field=text, properties.delta 增量),reasoning/tool 等非 text part 的增量隔离不进 回合;message.updated 只有 properties.info(role/messageID),不带文本, 仅用于探测新回合开始(role=user 首次见到该消息 id 时清空累积,同 id 重发忽略——session.diff 广播后服务端会重发同一 user 消息)
  • 回合结束主信号:session.status 的 properties.status.type=idle;同现的 session.idle 与顶层 idle/busy 事件全部忽略,防重复触发分类
  • 权限:permission.asked(properties.id 即 PermissionID,properties.permission/ patterns/metadata 拼描述);permission.replied 是应答回显,必须忽略
  • 其余类型(server.connected/heartbeat、session.updated/diff、catalog.updated、 integration.updated、reference.updated、plugin.added、step-start/step-finish、 tool/reasoning 等)宽容 Debug 跳过

Package opencode 提供 opencode server 的最小 HTTP/SSE 客户端与 serve 进程管理。

api.go —— opencode server 的 HTTP 客户端与 SSE 事件订阅。

职责:

  • 覆盖 MVP 所需的四个端点:POST /session、POST /session/{id}/prompt_async、 POST /session/{id}/permissions/{permID}、GET /event(SSE 事件流)
  • 统一的 basic auth(用户名固定 opencode,密码来自 OPENCODE_SERVER_PASSWORD)
  • SSE 断流自动指数退避重连(1s→2s→…→30s),直到 ctx 取消
  • 两个 http.Client 分工:sseClient 无 Timeout 供 SSE 长连接,httpClient 带 Timeout=30s 供一元调用(半死 server 不永久挂起,why 见 NewAPI 注释)

边界:

  • 不理解事件语义:事件类型字段含义、回合结束判定等语义归 adapter(Task 9), 本层只保证「事件 JSON 原样送达 onEvent」与「垃圾行不中断订阅」
  • 不管理进程生命周期(见 proc.go)

为什么 SSE 手写解析而不引第三方依赖:SSE 协议只有 data:/event:/空行三种形态, 手写按行解析约 40 行即可完全掌控。事件 payload 可能携带大段文本(plan、diff、 用户问答),必须能按需配置 scanner buffer(本实现 1MB)并按行处理超长 token; 第三方库的 buffer 上限与截断行为不可控,且我们需要的「未知行 Debug 跳过、绝不 中断」宽容语义由自己实现最精确,不依赖库的严格/宽松差异。

probe.go —— 只读存活探测。

职责:

  • Probe:读 proc.json,走 Proc.Alive 的既有判据(存活锁 + HTTP 应答), 如实返回存活结论

边界:

  • **绝不写**:不回收执行者进程、不占 runs 位、不碰 store、不发事件
  • 不打印 procInfo.Password:凭据值绝不进日志,要打只打非敏感字段

proc.go —— opencode serve 进程生命周期管理。

职责:

  • 组 `opencode serve --port <随机空闲端口> --hostname 127.0.0.1` 的 argv, 经 prochost 以 detached 方式拉起(shim 承载,见 prochost 包)
  • 密码与配置经 Spec.Env 注入(OPENCODE_SERVER_PASSWORD / OPENCODE_CONFIG), argv 不含任何秘密(why 见 serveSpec)
  • serve 输出落盘 <taskDir>/serve.log:serve 死亡诊断的持久 stderr
  • 就绪探测(轮询 GET /)、存活检查(存活锁 + HTTP 探活)、销毁(按进程组 Kill)

边界:

  • 不触碰会话:会话的创建、prompt、权限应答由 api.go 完成,本文件只保证 「serve 进程活着、端口可用」
  • 不生成任务级配置(OPENCODE_CONFIG 指向的文件由 Task 10 生成)

为什么进程经 prochost 而不是 agentd 直接 fork:agentd 重启或崩溃时子进程 树会被一并回收,正在执行的任务会无辜中断;prochost 的 shim 以新会话拉起并 持有存活锁,生命周期与 agentd 解耦——agentd 重启后靠 Alive() 探测发现存活 并重连 SSE。实况观测走 agentd 的 render 流式 endpoint(handoff attach)。

Package opencode 的提问通路纯逻辑。

职责:

  • renderQuestionTicket:把 opencode 的 question 请求渲染成协调者可读的工单文本
  • parseQuestionAnswers:把协调者的自由文本答复折算回 opencode 要的 answers

边界:

  • 全部是纯函数:不碰 runState、不发 HTTP、不打日志、不读时钟
  • 不认识 SSE 事件结构(解析在 adapter.go 的 mapQuestionAsked 里做)
  • 不做截断(由调用方用 turn.ClampQuestion 收口)

reap.go —— 无内存运行态时的确定性兜底回收。

职责:

  • Reap:按 proc.json 拿 prochost.Handle 并 Kill

边界:

  • 不碰任务状态(adapter 不写 store);回收不掉只返回错误,留不留事件是 manager 的事

reconcile.go —— 断连窗口的会话对账(B38)。

职责:

  • Reconcile:查会话尾部与持久化水位比对,把断连期间错过的回合终态补回事件流

边界:

  • 不写 store、不改任务状态:补发的事件经既有 evCh 交给 manager,状态迁移归它
  • 不发明事件语义:取回的文本交给既有的 turn.ParseTrailer 分类,产出与实时 路径同形的 question / result
  • **不捧回权限请求**:opencode 的消息流里 tool part 只有 callID 没有权限 id, 而 RespondPermission 要求真实 id、伪造即 404(更早的 spike 结论,见 adapter.go 的 onReconnect 降级告警)。建一张批了也送不回去的工单比不建更糟, 故 ReconcileOutcome.Pending 在本 adapter 恒为 0

resume.go —— 执行恢复:热重连与冷恢复。

职责:

  • Resume:按 ResumeReq 走恢复阶梯,返回实际走到的级别

边界:

  • 不判断「该不该恢复」的业务前提(如是否有未决权限工单)——那需要工单知识, 属 manager(见 manager.go 的 volatilePermitter)
  • 不改任务状态:adapter 不写 store(见 executor 包级边界)

taskenv.go —— opencode 任务环境物料生成。

职责:

  • WriteTaskEnv:在任务目录生成 opencode.json(权限收敛配置)与 prompt.md (回合制纪律 prompt,经 turn.RenderPrompt 渲染),供 serve 进程经 OPENCODE_CONFIG 注入(proc.go)与任务首回合 prompt 使用

边界:

  • 不启动进程、不发请求:serve 进程生命周期在 proc.go,HTTP 会话在 api.go
  • 回合协议(prompt 模板渲染 / trailer 解析 / git 取证 / 文本截断)在 internal/executor/turn 共享包,本文件不再持有

为什么 permission 是「静态分级」而非全 ask(2026-08-08 dogfooding 修正): 一期曾把 edit/bash 全部设为 ask,真实派发时协调者被 ls/grep/编辑测试文件 这类初级请求连环唤醒,审批噪音让审核流形同虚设——这恰是用户交互式用 opencode 时不存在的问题(用户在场且全局配置宽松)。修正后的分层:

  • edit: allow —— 在任务分支上改代码是派发的目的本身,diff 审核兜底; edit 保持 allow(2026-08-09 真机探针复核):越界写入由 external_directory: "ask" 拦截并升级人工,范围内写入本就该直接放行。 翻成 ask 等于给每次正常编辑加一道判完还是放行的空门(B27 复核结论, 见 docs/superpowers/plans/2026-08-09-permission-payload-probe.md §3.1)。
  • bash: 模式表 —— 危险模式(rm -rf/sudo/git push/reset --hard/--force/ curl/wget 等,见 bashPermissionRules)ask,其余 allow;
  • webfetch/external_directory: ask —— 外访与越出工作区仍逐次确认。

这是三级审批链的第 0 层(静态规则);第 1 层(廉价模型审批者)见二期 spec, 第 2 层是协调者/用户本人。

Index

Constants

This section is empty.

Variables

View Source
var ErrCustomAnswerRejected = errors.New("opencode 拒绝了自定义答案")

ErrCustomAnswerRejected 表示 opencode 拒绝了本次 reply 携带的答案——最可能的 原因是该问不接受自定义答案(服务端按选项 label 白名单校验)。

为什么要一个专门的哨兵:协调者填了一个不在选项里的答案时,调用方要把它 降级成「重问」而不是报一个语焉不详的 HTTP 错误。只有 4xx 归入本哨兵, 5xx 是服务端故障,与答案内容无关(见 ReplyQuestion)。

Functions

func WriteTaskEnv

func WriteTaskEnv(taskDir, taskID, model, planContent string) (configPath, promptPath string, err error)

WriteTaskEnv 在 taskDir 生成 opencode 配置与任务 prompt,返回二者路径。

参数:

  • taskDir: 任务工作目录(须已存在,由调用方保证)
  • taskID: 任务 ID,写入 prompt 标题行
  • model: 任务级模型覆盖(dispatch --model 折算而来);空则回退环境变量
  • planContent: 实现计划全文,原样嵌入 prompt 的「实现计划」段

返回:

  • configPath: 生成的 opencode.json 路径
  • promptPath: 生成的 prompt.md 路径
  • err: 渲染或写文件失败

注意:

  • 重复调用幂等覆盖:同名文件会被新内容覆盖,调用方可安全重试
  • 配置经结构体 marshal、prompt 经 text/template 渲染,均非字符串拼接

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API 是 opencode server 的最小 HTTP 客户端,持有 baseURL 与鉴权密码。

并发安全:字段构造后只读,可被多个 goroutine 同时使用。

func NewAPI

func NewAPI(baseURL, password string) *API

NewAPI 创建 opencode server 客户端。

参数:

  • baseURL: opencode serve 的地址(如 http://127.0.0.1:4345),尾斜杠会被剥掉
  • password: OPENCODE_SERVER_PASSWORD 的值,与用户名 opencode 拼成 basic auth

func NewAPIWithSSEBackoff

func NewAPIWithSSEBackoff(baseURL, password string, initial, max time.Duration) *API

NewAPIWithSSEBackoff 是 NewAPI 的 SSE 退避可注入变体:测试注入毫秒级退避, 让「成功连接后复位」的时间敏感断言不依赖真实 1s..30s 节奏;生产代码一律走 NewAPI 的默认退避。

参数:

  • initial/max: SSE 断流重连的初始/封顶退避(见 SubscribeEvents)

func NewAPIWithSSETiming

func NewAPIWithSSETiming(baseURL, password string, initial, max, stableAfter time.Duration) *API

NewAPIWithSSETiming 在退避区间之外再注入「连接算健康」的存活门槛, 供「退避复位按连接寿命而非按 200 响应」(A-8)的断言把门槛压到毫秒级。

参数:

  • stableAfter: 连接存活多久才算健康、才复位退避(生产默认 sseStableAfter)

func NewAPIWithUnaryTimeout

func NewAPIWithUnaryTimeout(baseURL, password string, timeout time.Duration) *API

NewAPIWithUnaryTimeout 是 NewAPI 的超时可注入变体:测试注入毫秒级短超时验证 「半死 server 不永久挂起」;生产代码一律走 NewAPI 的 30s 默认值。

参数:

  • timeout: 一元调用(httpClient)的超时;SSE 长连接(sseClient)不受影响

func (*API) CreateSession

func (a *API) CreateSession(ctx context.Context) (sessionID string, err error)

CreateSession 在 opencode server 上创建会话。

返回:

  • sessionID: 新建会话的 id,后续 PromptAsync / RespondPermission 都需要它
  • err: 请求或解析失败

func (*API) GetSession

func (a *API) GetSession(ctx context.Context, sessionID string) (d sessionDetail, err error)

GetSession 取单个会话的详情,用于把子会话归属回父任务。

参数:

  • ctx: 上下文;调用方负责叠加 ownershipTimeout
  • sessionID: 目标会话 id

返回:

  • sessionDetail: 会话详情
  • err: sessionID 为空、请求失败、非 2xx、响应解析失败时非 nil,此时详情为零值

注意:

  • sessionID 为空直接返回错误,不触达服务端:拿空 id 拼出的 "/session/" 只会 换来一个 404,白白占掉一次超时预算
  • 本方法在 SSE 事件回调里同步调用(见 adapter.resolveChildSession), 阻塞的是本任务的事件流——超时必须用 ownershipTimeout 而非 unaryTimeout

func (*API) HasSession

func (a *API) HasSession(ctx context.Context, sessionID string) (ok bool, err error)

HasSession 检查指定会话 id 是否仍存在于 serve 的会话列表里。

返回:

  • ok: 会话是否在场;GET /session 失败或列表解析失败时返回 (false, err)

注意:

  • 本方法只用于冷恢复的会话在场校验(spec §5.5.2):会话存在全局 sqlite, 进程重起不影响它,但要确认它真的还在——不能默认。不在就降级新会话

func (*API) LastAssistantMessage

func (a *API) LastAssistantMessage(ctx context.Context, sessionID string) (msg *SessionMessage, err error)

LastAssistantMessage 取会话里最后一条 assistant 消息(对账的数据源,B38)。

参数:

  • ctx: 控制单次请求超时
  • sessionID: 目标会话

返回:

  • (*SessionMessage, nil): 找到了。CompletedMS==0 表示该回合仍在进行
  • (nil, nil): 会话里还没有任何 assistant 消息——**合法状态,不是错误**
  • (nil, err): 请求或解析失败

注意:

  • 只看最后一条 assistant 消息就够,依据是「一个断连窗口内至多跨越一个回合 边界」(spec §2.2);不需要全量拉取比对
  • 权限请求**查不回来**:本端点的 tool part 只有 callID 没有权限 id,而 RespondPermission 要求真实 id、伪造即 404(更早的 spike 结论,见 adapter.go 的 onReconnect 降级告警)。故本方法不尝试提取权限

func (*API) ListPendingQuestions

func (a *API) ListPendingQuestions(ctx context.Context) (out []PendingQuestion, err error)

ListPendingQuestions 拉取当前全部挂起的提问请求(跨会话)。

返回:挂起请求列表;请求失败或解析失败时返回错误,列表为 nil

注意:

  • 返回的是**全部会话**的挂起请求,调用方必须按 SessionID 过滤出自己的
  • agentd 重启后重新发现挂起提问的唯一途径:SSE 无重放语义,重启窗口里 发生的 question.asked 永远收不到

func (*API) PromptAsync

func (a *API) PromptAsync(ctx context.Context, sessionID, text string) (err error)

PromptAsync 向会话发送一条 prompt,opencode 随即开始执行,函数立即返回。

参数:

  • sessionID: CreateSession 返回的会话 id
  • text: prompt 文本(计划内容、用户指令等)

注意:

  • 本调用不等待执行结果,执行事件通过 SubscribeEvents 消费

func (*API) RejectQuestion

func (a *API) RejectQuestion(ctx context.Context, requestID string) (err error)

RejectQuestion 拒绝一条挂起的提问,解除 question 工具的阻塞。

参数:requestID 为 question.asked 事件里的 properties.id

注意:

  • 用于「任务要停了但提问还挂着」的兜底解阻塞,不是协调者的正常答复通道

func (*API) ReplyQuestion

func (a *API) ReplyQuestion(ctx context.Context, requestID string, answers [][]string) (err error)

ReplyQuestion 把协调者的答案回填给 opencode 的 question 工具,工具随即返回、 回合继续。

参数:

  • requestID: question.asked 事件里的 properties.id
  • answers: 按问题顺序排列,每项是该问选中的 label 数组

返回:

  • 4xx 时返回可 errors.Is 命中 ErrCustomAnswerRejected 的错误(答案不被接受)
  • 其余失败返回普通错误

func (*API) RespondPermission

func (a *API) RespondPermission(ctx context.Context, sessionID, permID, response string) (err error)

RespondPermission 应答 opencode 的权限请求。

参数:

  • sessionID: 权限所属会话
  • permID: 权限请求 id(来自 SSE 事件的 permissionID 字段)
  • response: "once"(批准本次)或 "reject"(拒绝)

注意:

  • 非法 response 值直接返回错误,不触达服务端

func (*API) SubscribeEvents

func (a *API) SubscribeEvents(ctx context.Context, onEvent func(json.RawMessage), onReconnect func()) error

SubscribeEvents 订阅 GET /event(SSE),每收到一条事件就同步调用 onEvent(raw),直到 ctx 取消。

断流处理:连接意外断开(EOF/网络错误/服务端未就绪)后指数退避重连, 1s→2s→4s→…→30s 封顶,无限重试;ctx 取消时立即返回 nil。

参数:

  • onEvent: 每条事件的回调(同步调用:顺序有保证,但回调阻塞会暂停消费)
  • onReconnect: 断连后成功重连的回调(首次建连不触发);调用方借此把 「断连间隙可能丢事件」的告警交到业务层(P1-10b,可为 nil)

返回:

  • ctx 取消时返回**最后一次连接的失败原因**(最后一次连接健康则返回 nil): 调用方拿它填 FailReason(A-7)。恒返回 nil 会让「看门狗判死 → 事件流退出」 的失败现场只剩 "<nil>",零信息
  • 解析器遇到无法恢复的流异常(如单行超过 1MB 上限)时返回错误

注意:

  • 未知/解析失败的行(event: 行、注释、非 JSON data)Debug 跳过,绝不中断订阅

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter 是 opencode 的 executor.Adapter 实现(语义翻译层)。

并发安全:runs 表由 mu 保护;每个任务的运行态(回合累积、事件通道)只被 该任务自己的订阅 goroutine 访问,不做跨任务共享。

func New

func New(log *slog.Logger) *Adapter

New 创建 opencode adapter。

参数:

  • log: 本模块日志入口(nil 时退回 slog.Default())

func (*Adapter) Events

func (a *Adapter) Events(taskID string) <-chan executor.AdapterEvent

Events 返回任务的事件流通道(Start 后可用;Stop 或执行终结后关闭)。

注意(P1-11):任务不在运行(未启动/已 Stop/运行态已随终结注销)时返回 **已关闭的通道**而非 nil——契约是「通道关闭 = 执行终结」,消费方(manager 中介循环)靠 range 在关闭时退出;nil 通道会让 for-range 永久阻塞。Dispatch → go mediate 的调度窗口内 serve 若死亡,运行态已注销而中介循环尚未开始:返回 已关闭通道让中介循环立即退出、不泄漏 goroutine;该窗口内已产出的 failed 结果随运行态注销而丢失,是「尚未开始消费」的必然缝隙,任务状态由看门狗与 重启恢复兜底。

func (*Adapter) Probe

Probe 只读探测 opencode serve 是否仍存活(manager 的 prober 可选接口)。

判据与 Resume 共用同一份 Proc.Alive:存活锁被持有且端口有 HTTP 应答, 缺一即视为死亡。

参数:

  • req: 探测请求(TaskDir 是 proc.json 所在,即 DataDir/tasks/<id>)

返回:

  • Alive=true:serve 仍在
  • Alive=false + Note:已判死,Note 给协调者看
  • err != nil:探不出结论(proc.json 缺失/损坏),调用方按 unknown 处理

func (*Adapter) ProcHandle

func (a *Adapter) ProcHandle(taskID, taskDir string) (prochost.Handle, error)

ProcHandle 交出该任务的进程句柄(来自任务目录的 proc.json)。

参数:

  • taskID: 任务 ID,仅用于日志定位
  • taskDir: 任务目录(凭据所在)

返回:

  • 进程句柄;proc.json 不存在或不可解析时返回错误

注意:本方法**只读**,不探活、不发信号——存活判定与回收分别是 prochost.Alive 与 prochost.Sweep 的职责。agentd 以可选接口消费它 (不实现该方法的 adapter 一律按「无凭据」降级,与 reaper/prober 同款路数)。

func (*Adapter) Reap

func (a *Adapter) Reap(taskID, taskDir string) error

Reap 在没有内存运行态时按 proc.json 兜底回收 executor 侧资源。

回收顺序:读 proc.json 拿 Handle → prochost.Kill(内部先试锁,锁空闲直接成功)。

为什么不再有「确定性命名兜底」:旧实现在 proc.json 缺失时退到 tmux 会话名 handoff-<id8>,因为会话名可由 taskID 推导。锁+pid 无法从 taskID 推导, proc.json 缺失就是真的无据可查——如实报错交协调者,不猜。

返回:Handle 对应的进程本就不在时返回 nil——目标是「确保它没了」, 不是「确保我杀了它」。

func (*Adapter) Reconcile

func (a *Adapter) Reconcile(ctx context.Context, taskID string) (executor.ReconcileOutcome, error)

Reconcile 把断连期间错过的回合终态补回事件流。

参数:

  • ctx: 控制查询超时
  • taskID: 目标任务;运行态不在(未 Start / 已 Stop)时返回「无运行态」结论而非错误

返回:

  • ReconcileOutcome: 结论。Emitted 只可能是 0 或 1(spec §2.2 的不变量); Pending 恒为 0(见文件头注释)
  • err: 查会话失败。**调用方收到错误时只记 WARN、不改任何状态**——一次网络 抖动不该把能恢复的任务判成不可恢复

注意:

  • 是否补发由 WatermarkArmed 决定(见 proc.go 的 armed 语义):未 armed 的 legacy 会话**不补发**,只把当前尾部认作已消费基线——否则升级到本版本的 存量任务会在第一次恢复时集体重放最后一个回合;armed 会话空水位即「第一个 回合尚未消费」,必须补发(B38 头号场景)
  • 「补发 → 前进水位」在 turnMu 下串行完成,与实时路径的 mapIdle 互斥; 拆成两步会让两条路同时判「未消费」而补出两条终态

func (*Adapter) RespondPermission

func (a *Adapter) RespondPermission(ctx context.Context, taskID, permID, decision string) (err error)

RespondPermission 把协调者的权限裁决转发给 opencode server。

参数:

  • permID: 与 permission 事件中的 PermissionID 一致(manager 的 ticket id 经 taskID:permID 命名空间化,此处为裸 permID,由 manager 还原后传入)
  • decision: "once"(批准本次)或 "reject"(拒绝)

注意:

  • stopCh 已关时拒绝转发:与 Send 同因(见 Send 注释),保留态不接新裁决

func (*Adapter) Resume

func (a *Adapter) Resume(req executor.ResumeReq) (out executor.ResumeOutcome, err error)

Resume 重建 agentd 重启前已在执行的任务(spec §8「存活则重连 SSE 继续」): 从任务目录的 proc.json 恢复 serve 连接凭据并探活(存活锁 + HTTP 应答); 存活则重建 SSE 订阅、看门狗与事件通道,返回 Reattach。

参数:

  • req: 恢复请求(TaskDir 是 proc.json 所在,即 DataDir/tasks/<id>; RepoPath 用于重启后重新捕获 git 兜底分类的起点 commit 基线; SessionID 是落库的 opencode 会话 id)

返回:

  • Alive=false 时调用方(manager)把任务转 failed 交协调者裁决(保守优于静默)
  • err: 重建失败(proc.json 缺失/损坏、SessionID 为空),此时视为不可恢复

注意:

  • SessionID 为空时拒绝恢复:mapEvent 按会话 id 过滤事件,空 id 会把全部 事件当「其他会话」丢弃,静默恢复等于无声断流,宁可交协调者裁决
  • 重启时正在进行的回合文本累积在内存里已丢失:重建后的回合从 SSE 重放的新 快照重新累积(partSeen/partSnap 重新对账),idle 分类的 git 基线以重启 时刻的 HEAD 为准——这是 MVP 接受的缝隙,由 e2e 清单「agentd 重启」项 实测观察
  • 与 Start 的对称性:Stop(done 归档)对恢复出来的运行态同样有效, 会按进程组 Kill 回收执行者资源

func (*Adapter) Send

func (a *Adapter) Send(ctx context.Context, taskID, text string) (err error)

Send 向同一会话续发指令(原生续接:上下文完整保留)。

参数:

  • text: 协调者的回答/修改指令,原样透传,不得加工

注意:

  • stopCh 已关(Stop 已介入,运行态可能因 kill 失败被保留)时拒绝发送: 订阅已退出,prompt 发出也没有事件回程,任务会静默挂死——宁可让协调者 看到「任务不在运行」的明确错误
  • 有挂起的 question 请求时不发 prompt,改把答复回填给该请求(B49): question 工具阻塞时回合并未结束,发 prompt 会开出第二个回合

func (*Adapter) Start

func (a *Adapter) Start(ctx context.Context, req executor.StartReq) (err error)

Start 按「WriteTaskEnv → StartServe → 建会话 → 初始 prompt → 订阅映射」 流程启动任务执行并立即返回。

参数:

  • ctx: 控制启动阶段的超时/取消(CreateSession/PromptAsync 受其约束); 不代表执行生命周期(执行延续到 Stop)
  • req: 任务快照、计划原文与任务工作目录

返回:

  • 任一启动阶段失败(环境物料生成/serve 启动/建会话/发 prompt)返回错误, 调用方(manager)应把任务标记 failed

注意:

  • serve 已拉起但后续阶段失败时自动 Kill 清理进程残留,避免半启动进程占端口

func (*Adapter) Stop

func (a *Adapter) Stop(taskID string) (err error)

Stop 终止任务执行:取消订阅 → kill serve(执行者进程组)→ 事件通道关闭 → 注销运行态。

注意:

  • 幂等:重复 Stop 不 panic;事件通道只关闭一次(由订阅 goroutine 持有关闭权)
  • kill 失败但 serve 仍存活时**保留运行态**(P1-9):serve 占着端口与模型 会话,drop 掉就没有任何途径回收;保留期间运行态是惰性的(订阅与看门狗 都已退出、事件通道已关),Send/RespondPermission 经 stopCh 守卫拒绝继续执行
  • 保留态由 reapRetained 后台重试回收(A-10),重试有上限、放弃时打 Error 交人工(handoff stop 回收):**agentd 重启不会接走它**——RecoverOnStartup 只探测 running/waiting_answer 任务(watchdog.go),而 Stop 只由 Done 在 归档时调用(manager.go),进程内保留态只可能属于已归档任务
  • 运行态注销(drop)与 subscribeLoop 退出时的 drop 是幂等的 map 删除, mu 保护下不会重复释放——runs 表因此不随任务累积无界增长

type PendingQuestion

type PendingQuestion struct {
	ID        string         `json:"id"`
	SessionID string         `json:"sessionID"`
	Questions []QuestionInfo `json:"questions"`
}

PendingQuestion 是一条挂起的 question 请求(一次可含多道问题)。

type Proc

type Proc struct {
	Handle       prochost.Handle
	Port         int
	Password     string
	ServeLogPath string
}

Proc 描述一个运行中的 opencode serve 进程。

字段说明:

  • Handle: prochost 句柄(shim pid + 存活锁路径),存活与回收都靠它
  • Port: serve 监听的端口(127.0.0.1 上)
  • Password: 随机生成的 OPENCODE_SERVER_PASSWORD,api.go 的 basic auth 用它
  • ServeLogPath: serve 输出日志路径(<taskDir>/serve.log),serve 死后诊断只能读它

func StartServe

func StartServe(ctx context.Context, repoPath, taskID, taskDir, configPath string, env []string, log *slog.Logger) (*Proc, error)

StartServe 经 prochost 拉起 opencode serve 并等待就绪。

参数:

  • ctx: 上下文;就绪轮询同样受 ctx 取消影响
  • repoPath: 任务仓库路径,作为 serve 的工作目录(cwd)
  • taskID: 任务 id,用于日志与任务目录定位
  • taskDir: 任务目录(0700),serve.log 与 proc.json 都放这里
  • configPath: 任务级 opencode 配置路径(Task 10 生成),注入 OPENCODE_CONFIG
  • env: 额外注入的环境变量(形如 KEY=VALUE,来自 env 文件,已解析已展开); 覆盖顺序见 serveSpec 的 why
  • log: 本模块日志入口(StartServe 是进程启动点,日志需要显式传入而非走默认)

返回:

  • 就绪的 Proc;就绪 = 端口上已有 HTTP 服务响应(含 401:密码校验属后续请求的事, 这里只关心「serve 进程起来且 HTTP 层可应答」)
  • 错误:取端口/密码失败、写 proc.json 失败、拉起 shim 失败、10s 内未就绪 (错误信息携带 serve.log 尾部的 serve stderr)

注意:

  • 端口选择存在 TOCTOU 竞态(见 freePort),MVP 接受
  • 就绪超时后自动 Kill 清理残留进程,避免半启动进程占着端口

func (*Proc) Alive

func (p *Proc) Alive() bool

Alive 检查 serve 是否仍然存活:存活锁被持有 且 端口有 HTTP 应答。

两者缺一即视为死亡。锁证明 shim 还在,HTTP 证明 serve 本身还在应答—— serve 崩了但 shim 尚未收尸的窗口由 HTTP 这条兜住。

func (*Proc) Kill

func (p *Proc) Kill() error

Kill 终止 serve 及其后代(按进程组),幂等。

type QuestionInfo

type QuestionInfo struct {
	Question string           `json:"question"`
	Header   string           `json:"header"`
	Options  []QuestionOption `json:"options"`
	Multiple bool             `json:"multiple"`
	Custom   bool             `json:"custom"`
}

QuestionInfo 是 opencode question 工具的单个问题。

Multiple 为真表示该问可多选,Custom 为真表示该问接受选项之外的自定义答案。

type QuestionOption

type QuestionOption struct {
	Label       string `json:"label"`
	Description string `json:"description"`
}

QuestionOption 是一个问题的一个候选项。

type SessionMessage

type SessionMessage struct {
	ID          string
	Role        string
	CompletedMS int64
	ErrorText   string
	Text        string
	Finish      string
	ErrorName   string
	ToolStatus  string
}

SessionMessage 是会话里一条消息的最小形状(对账只需要这几个字段)。

字段说明:

  • ID: 消息 id,同时是对账水位的载体
  • Role: "assistant" | "user"
  • CompletedMS: 完结时刻(毫秒 epoch);**0 表示尚未完结**(消息未 finalize, 在飞或冻结),对账据此判「回合还在跑」
  • ErrorText: 非空表示该消息以错误告终(info.error 的原始 JSON)
  • Text: 该消息全部文本 part 的拼接结果,交给 turn.ParseTrailer 分类
  • Finish: 该消息的完结方式(info.finish),取值实测:""(缺席)/ "tool-calls" / "stop" / "unknown"。**只当正向结束标记用**:finish=="stop" 一定意味着回合 结束,但 finish=="tool-calls" 既可能是「中间工具消息、回合继续」也可能是 「被拒/工具报错而终」——不能反过来当「未结束」判据,须看 ToolStatus 消歧
  • ErrorName: 消息级错误的类型名(info.error.name);实测 "MessageAbortedError" 表示会话被 abort 而终(finish 缺席)
  • ToolStatus: 最后一条 tool part 的 state.status(取值实测 "running"/ "completed"/"error")。用于把「finish=tool-calls 的回合终态」与「真·回合 中途冻结」区分开:error=被拒/报错而终(补发),completed=中途冻结(不补发)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL