feat(tool,loop): 工具调用软判并回(裁决 14.4.2)
- 落点=Dispatcher.Invoke 入口(唯一执行口,覆盖主循环/子代理/编排全部调用方) - 语义=**不硬阻断**:越界调用照常执行,把 not_allowed 提示与软判理由挂到真实结果上回灌模型 (符合 design/14 §1「状态改变不构成能力边界、紧急时也能调」与 §4「入口前置软判」) - 允许集单一真源:loop 发布 capacity 过滤后的 schema 名单(= 模型本轮实际看到的工具面), 分发器不在本地另算一套判定 - 审计:复用既有 AuditEvent/OnAudit 通道(what=tool.reject.not_allowed,design/14 §4 原文), 前端小字与审计日志同源;日志带 source=runtime/tool - ctx 载体:Run 入口每 Run 播种允许集持有者(run.go +6 行,越界但必需——ctx 只能由 Run 或 request.go 播种,后者归 G5-A) - 与既有硬闸门零重叠:restrict(群聊/陌生人)与 ToolWhitelist(子代理)仍在更早处硬拦 + 各自审计 - 复核:`go build`/`go vet`/tool+loop 包测试绿,`-race` 下 SoftJudge/AllowSet 无竞态; 变异 M1(软判恒空)4 例红、M2(不发布允许集)仅端到端 1 例红 ⇒ 两侧各被独立覆盖
This commit is contained in:
1 parent
3972ea4007
commit
38b4527949
6 files changed
+675
-4
No files matched your search
@@ -13,11 +13,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
)
|
||||
@@ -106,9 +109,12 @@ type Option func(*Dispatcher)
|
||||
type Dispatcher struct {
|
||||
reg *registry.Registry
|
||||
dft time.Duration
|
||||
log *slog.Logger
|
||||
mu sync.RWMutex // guards timeouts and modes
|
||||
timeouts map[string]time.Duration
|
||||
modes map[string]string // tool name -> ExecutionMode* (default parallel)
|
||||
sinkMu sync.RWMutex // guards the soft-judge sink
|
||||
sink SoftJudgeSink
|
||||
}
|
||||
|
||||
// New builds a Dispatcher backed by reg, applying opts in order. It returns an
|
||||
@@ -121,6 +127,7 @@ func New(reg *registry.Registry, opts ...Option) (*Dispatcher, error) {
|
||||
d := &Dispatcher{
|
||||
reg: reg,
|
||||
dft: DefaultTimeout,
|
||||
log: sourcedLog(nil),
|
||||
timeouts: make(map[string]time.Duration),
|
||||
modes: make(map[string]string),
|
||||
}
|
||||
@@ -138,6 +145,22 @@ func WithDefaultTimeout(timeout time.Duration) Option {
|
||||
return func(d *Dispatcher) { d.dft = timeout }
|
||||
}
|
||||
|
||||
// WithLogger sets the dispatcher's base logger (used by the allow-set soft
|
||||
// judge, design/14 §4). Nil restores the discard logger. The stored logger is
|
||||
// tagged with the module source (project rule §10).
|
||||
func WithLogger(l *slog.Logger) Option {
|
||||
return func(d *Dispatcher) { d.log = sourcedLog(l) }
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if l == nil {
|
||||
l = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
return logger.WithSource(l, "runtime/tool")
|
||||
}
|
||||
|
||||
// Register registers t as a tool with the underlying capability registry,
|
||||
// delegating duplicate/invalid handling to the registry. A nil tool is rejected
|
||||
// by the registry's own validation.
|
||||
@@ -207,6 +230,12 @@ func (d *Dispatcher) PolicyEngine(env restrict.ActorEnv) *restrict.Policy {
|
||||
// default). Any failure — unknown tool, invalid args, timeout, panic or an
|
||||
// Execute error — is returned as a *ToolError wrapping the matching sentinel,
|
||||
// so it is safe to feed back to the LLM.
|
||||
//
|
||||
// A call outside the round's allow set (mount/publish with
|
||||
// WithRoundAllowSet/PublishRoundAllowSet, see allowset.go) is also returned as a
|
||||
// *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).
|
||||
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}
|
||||
@@ -239,6 +268,10 @@ func (d *Dispatcher) Invoke(ctx context.Context, name string, args map[string]an
|
||||
}
|
||||
}
|
||||
|
||||
// 允许集前置软判(design/14 §4,裁决 14.4.2):越出本轮允许集的调用
|
||||
// 不硬阻断(状态不是能力边界),但必须可观测、可回灌 —— 见 allowset.go。
|
||||
notice := d.softJudge(ctx, name)
|
||||
|
||||
invokeCtx, cancel := d.invokeCtx(ctx, name)
|
||||
defer cancel()
|
||||
|
||||
@@ -246,6 +279,9 @@ func (d *Dispatcher) Invoke(ctx context.Context, name string, args map[string]an
|
||||
if execErr != nil {
|
||||
return nil, wrapInvokeErr(name, execErr)
|
||||
}
|
||||
if notice != "" {
|
||||
return SoftNotAllowed{Tool: name, NotAllowed: notice, Result: result}, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user