feat(tool): 写类工具文件级互斥(读共享 / 写独占)
新增 tool/fileclaims.go:FileTargeter 可选接口,由工具声明本次调用要碰的文件 (动态声明、不猜正文 —— 同一工具的 path 在读/写语义不同)。 语义:读共享 / 写独占;两个读允许并行(主人「优先性能」口径);未知操作/裸路径按写处理(保守)。 占用键含操作类别(read/write,NUL 分隔),路径 Abs+Clean+Windows 大小写折叠;随最后占用者离场删键(有界)。 Dispatcher 默认装配进程内占用表(无需 cmd 侧接线);Invoke 登记 + defer 释放(成功/报错/超时/panic 四条路径)。 loop/tools.go 唯一接线:tool.WithLegID(ParallelLegFrom(ctx)) 桥接网关路标识。 判忙不阻塞、不排队死等;显式文案回灌(含文件名与状态)让 LLM 稍后重试。 测试 3 个新文件(占用表语义 / 接线确定性并发 / loop 桥与端到端);变异 5 条,另揪出并修一个 真隐患(空串既当「无人占用」哨兵又是合法路标识 ⇒ 改 (leg,busy) 返回)。 诚实边界:①「1 普通回合 + 1 并行路」时普通回合不受保护(网关只给 parallel 路盖 legID; 修法须与 checkpoint 归属一起裁决);② 目前无真实工具实现 FileTargeter ⇒ 机制就绪但占用表恒空, 真实保护待下一片给 file 等工具加声明。
This commit is contained in:
1 parent
c0dc435770
commit
354b08a45f
7 files changed
+1367
-12
No files matched your search
@@ -110,9 +110,10 @@ type Dispatcher struct {
|
||||
reg *registry.Registry
|
||||
dft time.Duration
|
||||
log *slog.Logger
|
||||
mu sync.RWMutex // guards timeouts and modes
|
||||
mu sync.RWMutex // guards timeouts, modes and the file claimer
|
||||
timeouts map[string]time.Duration
|
||||
modes map[string]string // tool name -> ExecutionMode* (default parallel)
|
||||
claimer FileClaimer // 文件级互斥的占用表(见 fileclaims.go)
|
||||
sinkMu sync.RWMutex // guards the soft-judge sink
|
||||
sink SoftJudgeSink
|
||||
}
|
||||
@@ -120,6 +121,11 @@ type Dispatcher struct {
|
||||
// New builds a Dispatcher backed by reg, applying opts in order. It returns an
|
||||
// error wrapping ErrNilRegistry when reg is nil. The default per-invoke timeout
|
||||
// is DefaultTimeout unless overridden with WithDefaultTimeout.
|
||||
//
|
||||
// 默认装配一份进程内文件占用表(fileclaims.go):它对**未声明目标文件**的工具、
|
||||
// 以及**未挂路标识**(单路 / 子代理 / 未接线)的调用完全无副作用,因此既有行为
|
||||
// 逐字不变;一旦工具实现 FileTargeter 且调用带路标识,写类工具的文件级互斥立即
|
||||
// 生效(无需上层再接线)。
|
||||
func New(reg *registry.Registry, opts ...Option) (*Dispatcher, error) {
|
||||
if reg == nil {
|
||||
return nil, fmt.Errorf("tool: new: %w", ErrNilRegistry)
|
||||
@@ -130,6 +136,7 @@ func New(reg *registry.Registry, opts ...Option) (*Dispatcher, error) {
|
||||
log: sourcedLog(nil),
|
||||
timeouts: make(map[string]time.Duration),
|
||||
modes: make(map[string]string),
|
||||
claimer: NewFileClaimer(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
@@ -152,6 +159,14 @@ func WithLogger(l *slog.Logger) Option {
|
||||
return func(d *Dispatcher) { d.log = sourcedLog(l) }
|
||||
}
|
||||
|
||||
// WithFileClaimer 替换文件级互斥使用的占用表(设计/实现见 fileclaims.go)。
|
||||
// 传 nil 表示**关闭**文件级互斥(工具声明的目标一律不登记、不检查)—— 供测试
|
||||
// 与需要「完全不做互斥」的装配点使用;新构建的 Dispatcher 默认已带一份进程内
|
||||
// 占用表,通常无需调用它。
|
||||
func WithFileClaimer(c FileClaimer) Option {
|
||||
return func(d *Dispatcher) { d.claimer = c }
|
||||
}
|
||||
|
||||
// sourcedLog tags l with the module source "runtime/tool"; nil becomes a
|
||||
// discard logger, so the soft judge can always log safely.
|
||||
func sourcedLog(l *slog.Logger) *slog.Logger {
|
||||
@@ -236,6 +251,13 @@ func (d *Dispatcher) PolicyEngine(env restrict.ActorEnv) *restrict.Policy {
|
||||
// *ToolError-free soft result: the call still runs, and its value comes back as
|
||||
// SoftNotAllowed carrying the not_allowed notice for the model (design/14 §4,
|
||||
// 裁决 14.4.2).
|
||||
//
|
||||
// 参与文件级互斥的工具(实现 FileTargeter,见 fileclaims.go)在目标文件正被
|
||||
// **另一条在途路**操作时,其调用**不会执行**,而是直接返回 ErrFileBusy(带文件
|
||||
// 状态的中文文案)。它**不包 ToolError**:占用判定发生在执行之前,回灌给模型的
|
||||
// 结果就该是一句话(ToolError 的 message+cause 渲染会把同一句话打印两遍);
|
||||
// 循环的 errorKind 仍把它归为 execution,与执行面失败的分类一致。
|
||||
// 无论如何都**不静默、不排队等待**。
|
||||
func (d *Dispatcher) Invoke(ctx context.Context, name string, args map[string]any) (any, error) {
|
||||
if name == "" {
|
||||
return nil, &ToolError{Name: name, Message: "tool name is empty", cause: ErrUnknownTool}
|
||||
@@ -272,6 +294,18 @@ func (d *Dispatcher) Invoke(ctx context.Context, name string, args map[string]an
|
||||
// 不硬阻断(状态不是能力边界),但必须可观测、可回灌 —— 见 allowset.go。
|
||||
notice := d.softJudge(ctx, name)
|
||||
|
||||
// 文件级互斥(design/15 §2.5.7、主人 2026-10-05 裁决 3):写类工具执行前先登记
|
||||
// 本次要动的文件;被**另一条在途路**占住时立刻把带文件状态的 ErrFileBusy 交给
|
||||
// 模型(不静默、不排队死等)。释放挂 defer:成功 / 报错 / 超时 / panic 都释放,
|
||||
// 绝不把文件永久锁住。未挂路标识或工具未声明目标 ⇒ 完全不参与互斥,逐字不变。
|
||||
release, cerr := d.claimFiles(ctx, name, tool, args)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
if release != nil {
|
||||
defer release()
|
||||
}
|
||||
|
||||
invokeCtx, cancel := d.invokeCtx(ctx, name)
|
||||
defer cancel()
|
||||
|
||||
|
||||
Reference in new issue
Block a user