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 }