Files
Cyrene_v2/internal/runtime/tool/legfile.go
T
AskaEth 89e13cf53d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,loop): 忙时并行回合基础管线(I1′ 有条件放宽)
- turnId 由编排器生成、loop Run 复用(硬前提:否则 turn_begin 与流帧各铸一个,客户端两级分桶立即失配)
- StateSink 增带 turnId 的变体(未接线时退化为现签名,不丢状态帧)
- 上行 chat.parallel(WS chat 帧 + REST 三入口同形;省略 = 逐字不变)
- 单会话额度默认 2 / 硬上限 3 / <2 关闭;额度满或未声明 ⇒ 落回既有排队语义
- I1′:checkpoint 归属、历史配对、忙态计数三处从会话级单例升为路级
- session_idle 收紧为「所有在途路收尾且 pending 空」(仅并行时生效;单路逐字不变)
- 预留接口(形状定死、无实现无接线):tool/legfile.go 的 FileClaimer / ErrFileBusy / WithLegID
- 顺手修一个真 bug:并行路 ctx 派生自提交请求作用域,REST 同步入口一写响应就把它连坐杀死(改 context.WithoutCancel 派生)
- 23 个新用例(含 -race -count=3);变异 6 条(turnId 复用 / idle 时点 / 并行升级 / checkpoint 键 / 编排器交 id / pending_ack reason)
- 契约变更点 10 条待监工升 v0.65(本人未改 Docs/**)
2026-10-05 09:18:50 +08:00

81 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tool
import (
"context"
"errors"
"fmt"
)
// 本文件是**接口预留**(design/15 §2.5.7 与主人 2026-10-05 裁决 3),本切片
// **不实现也不接线**:忙时并行(design/15 §2.5)落地后,同一会话可以有多条在途
// 回合,而写类工具(执行命令 / 修改文件 / 对外发消息 / 改配置,即 design/11 §4
// 的敏感清单)在两路之间会真正争抢同一个副作用目标。
//
// 主人口径(裁决 3)是**文件级**互斥,而不是会话级串行:
// - 工具先检测目标文件此刻是否正被**别的在途路**操作;
// - **同一文件**正在被操作 ⇒ 把「该文件正在被操作」**显式**作为工具结果返回给
// LLM,并提示稍后再试(**不静默、不排队死等**);
// - **不是同一个文件** ⇒ 允许并行操作。
//
// 为什么只留接口:写类工具的「目标文件」需要逐工具声明(命令、补丁、配置写入
// 各自的目标语义不同),且占用登记要挂在**路**的生命周期上(路一收尾就必须释放,
// 否则一个中途报错的路会把那个文件永久锁住)。这两件都属于后续切片,本切片只把
// 通道与判定口径钉死,使实现方不必再改工具接口。
//
// 身份与权限**不走这条通道**:它只回答「此刻有没有另一路正在动这个文件」,与
// actor 元数据 / 策略引擎无关(项目红线)。
// ErrFileBusy 报告目标文件此刻正被另一条在途路操作。它不是执行失败:调用方
// (写类工具)应把它**显式**返回给 LLM,让模型稍后重试,而不是静默吞掉或排队
// 死等(主人裁决 3)。错误文案面向模型,因此写清「同一文件被另一路占用」与
// 「请稍后再试」,绝不泄露另一路的内容。
var ErrFileBusy = errors.New("tool: 目标文件正在被另一条并行回合操作,请稍后再试")
// FileBusyError 把 ErrFileBusy 与具体目标绑成一条可直接交给 LLM 的错误,
// 同时保持 errors.Is(err, ErrFileBusy) 成立(%w 包装)。target 是**服务端
// 规范化后**的文件标识,调用方不得把用户原始输入当作它(那会把路径穿越或
// 隐私内容带进模型上下文)。
func FileBusyError(target string) error {
return fmt.Errorf("%w(目标:%s)", ErrFileBusy, target)
}
// FileClaimer 是按**文件级**粒度登记在途操作的接口(主人裁决 3)。实现方
// (后续切片)负责:
// - 以规范化后的目标为键,记录「哪一条路正在操作它」;
// - Claim 在目标已被**另一条路**占用时返回 ErrFileBusy(同一路重复 Claim
// 同一目标必须是幂等的,否则一条路自己的多步工具调用会互相挡);
// - Release 无条件释放该路在该目标上的占用(路收尾时的兜底),legID 不是
// 占用者时是空操作。
//
// 实现必须是**有界的**:占用表随路收尾清空,绝不随会话长度增长。
type FileClaimer interface {
Claim(ctx context.Context, legID, target string) error
Release(ctx context.Context, legID, target string)
}
// legIDKey 是「本回合属于哪条并发路」的 context key。
type legIDKey struct{}
// WithLegID 把本回合所属的并发路标识挂到 ctx 上。循环在 Run 顶部按 ctx 上挂的
// 路标识播种一次(单路 / 子代理 / 后台思考为空串 ⇒ 不挂载),工具经
// Dispatcher.Invoke 的 ctx 读到它,从而知道「我是哪一路」——这是文件级占用
// 登记的唯一来源,绝不从模型输出推断。
//
// 空 legID 是空操作:单路时未挂载,读取侧返回空串,与加入本特性之前逐字相同。
func WithLegID(ctx context.Context, legID string) context.Context {
if ctx == nil || legID == "" {
return ctx
}
return context.WithValue(ctx, legIDKey{}, legID)
}
// LegIDFrom 返回 ctx 上挂的并发路标识;未挂载(单路、子代理、未接线)时返回
// 空串。空串表示「本回合不与其他路并行」,此时文件级互斥**无需生效**。
func LegIDFrom(ctx context.Context) string {
if ctx == nil {
return ""
}
s, _ := ctx.Value(legIDKey{}).(string)
return s
}