turn

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: 8 Imported by: 0

Documentation

Overview

fallback.go —— 「回合结束但没有协议 trailer」时的共用裁决。

职责:

  • 构造无 trailer 且 git 有新提交时的回合结果(一律 OK=false)
  • 构造该结果的失败原因文案,保证判定依据 / git 实况 / 正文尾部三者齐全

边界:

  • 不查 git(那是 GitTurnStatus),不判断 hasNew:调用方把结论传进来
  • 不处理 !hasNew 与 git 查询失败两条分支:它们仍转 question,由各 adapter 自行处置(各家的空文本守卫与原生提问抑制不同,强行统一会丢掉这些差异)
  • 纯函数:不打日志。判定结果由各 adapter 在调用点记录(那里才有 taskID)

为什么这段判定必须共用:四个 adapter 曾各写一份,已经漂移——opencode 的 summary 取回合末 200 字,grok/codex 取一句固定文案,同一个判定给协调者看的 东西完全不同。四份副本各自漂移正是 B74 这类问题的温床。

Package turn 提供 executor 无关的「回合协议」:教模型协议的 prompt 模板、 解析模型输出的 trailer、回合取证与文本工具。

职责:

  • RenderPrompt:把实现计划渲染成带回合纪律(提问/收尾/不切分支)的启动 prompt
  • ParseTrailer:从回合末文本宽容提取协议 JSON(ask/finish)
  • GitTurnStatus:trailer 缺失时以「是否有新提交」作事实裁决
  • 文本截断与 render.log 追加等两 adapter 共用的小工具

边界:

  • 不认识任何具体 executor(opencode/grok/claude),不发请求、不起进程
  • 不做状态机迁移、不写 store:只做纯变换与两个受限 I/O(git 只读、日志追加)

为什么 prompt 模板与 ParseTrailer 必须同包:教模型协议的 prompt 与解析协议的 代码是同一契约的两半,分居两处必然出现「改纪律只改一半」的漂移——两个 executor 的协调者会看到不一样的东西。

Index

Constants

View Source
const QuestionTextLimit = 8000

QuestionTextLimit 是交给协调者的回合文本上限。兜底分类会把整个回合原文当 question 发出,一个失控的长回合会直接灌进工单行与协调者终端;全文始终在 任务目录的 render.log 里,截断不丢证据。

为什么导出:opencode 的 regression_group_a_test.go 直接断言这个上限, 搬包后它得能从 turn 引到同一个值——两处各写一个 8000 就会悄悄漂移。

Variables

This section is empty.

Functions

func AppendRender

func AppendRender(renderLogPath, delta string) error

AppendRender 把 delta 追加到 renderLogPath(不存在则创建,权限 0644)。

注意:调用方通常在高频文本增量路径上调用本函数,失败应只 Warn 不中断回合 ——可见性是增强能力,不值得为它挂掉任务。

func ClampQuestion

func ClampQuestion(text string) string

ClampQuestion 把兜底分类产出的整段回合文本收敛到 QuestionTextLimit, 超出时追加尾缀指明全文去处。

为什么**不能**复用 TruncateMarked:两者的「全文在哪」不同,尾缀因此必须不同。

  • TruncateMarked 用于 permission 文本,全文在工单里(B6 契约:工单存全文、 事件截断),协调者 `handoff show` 就能拿到,`…(已截断)` 足够;
  • 本函数用于 question 文本,全文**不在工单里**,只在任务目录的 render.log。 不指路 = 协调者拿到半截文本且不知道去哪找全文,证据链断掉。

这段尾缀是逐字从 opencode 现有实现搬来的,opencode 的 regression_group_a_test.go 断言 `strings.Contains(ev.Text, "render.log")`, 改字面量即回归。

func GitTurnStatus

func GitTurnStatus(repoPath, startCommit string) (branch, commit string, hasNew bool, err error)

GitTurnStatus 返回工作区当前分支、HEAD commit,以及相对 startCommit 是否有新提交。

参数:

  • repoPath: 任务工作目录(仓库或 worktree 路径)
  • startCommit: 回合起点的 HEAD;空串表示起点未知,此时 hasNew 恒为 false

返回:分支名、HEAD hash、是否有新提交、错误

为什么需要它:模型可能不守收尾纪律(不输出 trailer)。此时唯一可信的是 git 实况——有新提交才可能是「干完了」,没有就该交协调者,绝不替模型宣布完成。

func NoTrailerFailReason

func NoTrailerFailReason(branch, commit, text string) string

NoTrailerFailReason 构造「回合未输出协议 trailer」的失败原因文案。

参数:

  • branch, commit: GitTurnStatus 查到的 git 实况
  • text: 回合正文全文(本函数负责截尾)

返回:一条同时包含判定依据、git 实况、正文尾部的文案

注意:三者缺一,协调者就得回去翻日志——这条要求来自 spec §3.2,不是格式偏好。

func NoTrailerResult

func NoTrailerResult(sessionID, branch, commit, text string) *executor.Result

NoTrailerResult 构造「无 trailer 但 git 有新提交」时的回合结果。

参数:

  • sessionID: executor 会话标识,供续接与归档
  • branch, commit: GitTurnStatus 查到的 git 实况
  • text: 回合正文全文

返回:OK=false 的结果,git 实况保留在结构化字段里

为什么是 OK=false 而不是 OK=true:模型没有宣布完成,handoff 不替它宣布。 翻转不给协调者增加任何一次操作——OK 与 !OK 都落到 waiting_review, 而 done 与 continue 在该状态下都合法。变的只是那条事件从「已完成,摘要如下」 (邀请协调者不看 diff 就 done)变成「有新提交,但模型未按纪律宣布完成」 (要求看一眼)。代价为零,收益是不再有假完成。

注意:Branch/CommitHash 必须继续填。翻转若把 git 实况降级成一段自由文本, 协调者与任何下游都无法再结构化地取用它。

func RenderPrompt

func RenderPrompt(taskID, planContent string) (string, error)

RenderPrompt 渲染带回合纪律的启动 prompt。

参数:

  • taskID: 任务 ID,写入 prompt 标题行
  • planContent: 实现计划全文(dispatch 侧已把 --prompt 附加指令拼在其后), 原样嵌入「实现计划」段,本函数不再二次拼接

返回:渲染后的 prompt 全文;模板执行失败时返回错误

func TailRunes

func TailRunes(s string, n int) string

TailRunes 返回末尾 n 个 rune;不足 n 时原样返回。

func TruncateMarked

func TruncateMarked(s string, n int) string

TruncateMarked 按 rune 截断到 n,确实截断时追加 executor.TruncationMarker。

为什么必须带标记:上层据此 fail-closed——权限文本含标记说明裁决者看到的是 不完整命令,危险片段可能落在截断之外,黑名单与廉价模型都不可信。

func TruncateRunes

func TruncateRunes(s string, n int) string

TruncateRunes 按 rune 截断到 n,不加任何标记。

Types

type Trailer

type Trailer struct {
	Question string // ask 类型:需要人决策的问题
	Branch   string // finish 类型:提交所在分支
	Commit   string // finish 类型:提交 hash
	Summary  string // finish 类型:50 字内摘要
}

Trailer 是从回合末消息文本提取出的协议数据。

func ParseTrailer

func ParseTrailer(text string) (kind string, t Trailer)

ParseTrailer 从回合末文本宽容提取协议 JSON(ask/finish)。

提取分两级:

  • 主路径:最后一个非空行,从该行第一个 { 起解码一个 JSON 值(容忍前缀 与后缀正文)
  • 回退:主路径无果时,取最后一个「以 { 开头」的行按整行解码(旧规则)

返回:

  • kind: "ask"(附 Question)| "finish"(附 Branch/Commit/Summary)| "none"
  • t: 解析出的协议数据;kind 为 "none" 时为零值

注意:

  • 放宽只作用于最后一个非空行:正文中间复述协议 JSON 不会被误当成结论
  • 找不到或 JSON 损坏时返回 "none",绝不 panic(模型输出不可信,防御在边界上做)
  • 纯函数:不打日志,由调用方记录提取结果

Jump to

Keyboard shortcuts

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