抽屉优化

This commit is contained in:
pphua committed 2026-08-25 12:05:26 +08:00
1 parent 7234874797
commit d968727460
14 files changed
+669 -154

No files matched your search

+14 -14
View File
@@ -8,11 +8,11 @@ import (
"path/filepath"
"strings"
"github.com/Autumn-27/norma/skill"
actool "github.com/Autumn-27/norma/tool"
"github.com/Autumn-27/artex/agent"
"github.com/Autumn-27/artex/db"
"github.com/Autumn-27/artex/traffic"
"github.com/Autumn-27/norma/skill"
actool "github.com/Autumn-27/norma/tool"
)
// wireAgentAugment connects the PG agent_visibility table into the agent runtime:
@@ -155,7 +155,7 @@ func wireAgentAugment(pg *db.DB, skillDir string, hostTools func() ([]actool.Cor
if !contains(boundAgents, a.Key) {
continue // only defer names this agent is actually bound to
}
allNames = append(allNames, name) // schema withheld from the prompt
allNames = append(allNames, name) // schema withheld from the prompt
globalNames = append(globalNames, name) // advertised in the deferred block
globalSet[name] = true
if unlock != nil {
@@ -235,7 +235,7 @@ func wireTools(pg *db.DB, domainReg map[string]actool.CoreTool) {
// interactive_shell is on, so Bash points at shell_open for interactive programs
// without ever referencing a tool that isn't injected (§14.1/§14.2).
const bashInteractiveShellNote = "\n\n需要【交互输入】的程序(msfconsole / ssh 交互登录 / mysql、psql、python 等 REPL / 密码或 yes/no 提示 / nc 反弹 shell)不要用 Bash(它没有 stdin、会卡住),改用 shell_open 开交互会话(用完 shell_close)。一次性、非交互命令仍用 Bash。"
agent.ToolResolve = func(_ context.Context, agentKey string, tools []actool.CoreTool) []actool.CoreTool {
agent.ToolResolve = func(ctx context.Context, agentKey string, tools []actool.CoreTool) []actool.CoreTool {
rows, err := pg.ListTools()
if err != nil {
log.Printf("[tools] 读取工具表失败,按代码默认放行: %v", err)
@@ -245,6 +245,14 @@ func wireTools(pg *db.DB, domainReg map[string]actool.CoreTool) {
for _, t := range rows {
byKey[t.Key] = t
}
runInfo := agent.RunInfoFrom(ctx)
resolve := func(t actool.CoreTool, row *db.Tool) actool.CoreTool {
var schema map[string]any
if len(row.Schema) > 0 {
_ = json.Unmarshal(row.Schema, &schema)
}
return meterTool(agent.DecorateTool(t, row.Description, schema), pg, row.Key, agentKey, runInfo)
}
out := tools[:0:0]
for _, t := range tools {
row, known := byKey[t.Name()]
@@ -255,11 +263,7 @@ func wireTools(pg *db.DB, domainReg map[string]actool.CoreTool) {
if !row.Enabled || !contains(row.Agents, agentKey) {
continue // disabled globally or not bound to this agent → drop
}
var schema map[string]any
if len(row.Schema) > 0 {
_ = json.Unmarshal(row.Schema, &schema)
}
out = append(out, agent.DecorateTool(t, row.Description, schema))
out = append(out, resolve(t, row))
}
// inject: domain tools bound to this agent in the DB but absent from the
// incoming list. Covers agents (Auto, custom) whose base only has DefaultTools
@@ -279,11 +283,7 @@ func wireTools(pg *db.DB, domainReg map[string]actool.CoreTool) {
if !ok {
continue // not a domain tool; custom/host tools are injected via hostTools()
}
var schema map[string]any
if len(row.Schema) > 0 {
_ = json.Unmarshal(row.Schema, &schema)
}
out = append(out, agent.DecorateTool(inst, row.Description, schema))
out = append(out, resolve(inst, row))
}
}
// shell hints: user-defined kind="shell" tools are not callable — they are
+10
View File
@@ -734,6 +734,16 @@ func (s *Server) pgListTools(w http.ResponseWriter, r *http.Request) {
if ts == nil {
ts = []*db.Tool{}
}
// Usage is best-effort decoration. Runtime tool resolution keeps using the plain
// catalog query, so agent assembly never pays for this aggregate.
counts, countErr := pg.ToolUsageCounts()
if countErr != nil {
log.Printf("[tools] 读取调用统计失败: %v", countErr)
} else {
for _, tool := range ts {
tool.Calls = counts[tool.Key]
}
}
writeJSON(w, 200, map[string]any{"tools": ts})
}
+48
View File
@@ -0,0 +1,48 @@
package server
import (
"context"
"encoding/json"
"log"
actool "github.com/Autumn-27/norma/tool"
"github.com/Autumn-27/artex/agent"
"github.com/Autumn-27/artex/db"
)
type toolUsageRecorder interface {
InsertToolUsage(*db.ToolUsage) error
}
// meteredTool records one row immediately before a catalog tool is invoked. It
// embeds the resolved tool so schema overrides, defaults and permission behavior
// remain unchanged.
type meteredTool struct {
actool.CoreTool
recorder toolUsageRecorder
toolKey string
agentKey string
ri agent.RunInfo
}
func meterTool(t actool.CoreTool, recorder toolUsageRecorder, toolKey, agentKey string, ri agent.RunInfo) actool.CoreTool {
if recorder == nil {
return t
}
return &meteredTool{CoreTool: t, recorder: recorder, toolKey: toolKey, agentKey: agentKey, ri: ri}
}
func (m *meteredTool) Call(ctx context.Context, input json.RawMessage, tc *actool.ToolContext) (actool.Result, error) {
if err := m.recorder.InsertToolUsage(&db.ToolUsage{
ToolKey: m.toolKey,
AgentKey: m.agentKey,
TaskID: m.ri.TaskID,
ExplorationID: m.ri.ExplorationID,
IntentID: m.ri.IntentID,
SessionID: m.ri.SessionID,
}); err != nil {
log.Printf("[toolusage] insert %s: %v", m.toolKey, err)
}
return m.CoreTool.Call(ctx, input, tc)
}
+73
View File
@@ -0,0 +1,73 @@
package server
import (
"context"
"encoding/json"
"errors"
"testing"
actool "github.com/Autumn-27/norma/tool"
"github.com/Autumn-27/artex/agent"
"github.com/Autumn-27/artex/db"
)
type recordingToolUsage struct {
rows []*db.ToolUsage
err error
}
func (r *recordingToolUsage) InsertToolUsage(row *db.ToolUsage) error {
copy := *row
r.rows = append(r.rows, &copy)
return r.err
}
func TestMeterToolRecordsAttributionAndDelegates(t *testing.T) {
calls := 0
base := actool.Build(actool.Spec{
Name: "record_fact",
Run: func(context.Context, json.RawMessage, *actool.ToolContext) (actool.Result, error) {
calls++
return actool.Result{}, nil
},
})
recorder := &recordingToolUsage{}
ri := agent.RunInfo{TaskID: 42, ExplorationID: 8, IntentID: 9, SessionID: "session-1"}
wrapped := meterTool(base, recorder, "record_fact", "worker", ri)
if _, err := wrapped.Call(context.Background(), json.RawMessage(`{"secret":"not persisted"}`), nil); err != nil {
t.Fatal(err)
}
if calls != 1 {
t.Fatalf("delegate calls: want 1, got %d", calls)
}
if len(recorder.rows) != 1 {
t.Fatalf("usage rows: want 1, got %d", len(recorder.rows))
}
got := recorder.rows[0]
if got.ToolKey != "record_fact" || got.AgentKey != "worker" || got.TaskID != 42 ||
got.ExplorationID != 8 || got.IntentID != 9 || got.SessionID != "session-1" {
t.Fatalf("unexpected attribution: %+v", got)
}
}
func TestMeterToolFailureDoesNotBreakInvocation(t *testing.T) {
calls := 0
base := actool.Build(actool.Spec{
Name: "list_facts",
Run: func(context.Context, json.RawMessage, *actool.ToolContext) (actool.Result, error) {
calls++
return actool.Result{}, nil
},
})
recorder := &recordingToolUsage{err: errors.New("ledger unavailable")}
wrapped := meterTool(base, recorder, "list_facts", "worker", agent.RunInfo{})
if _, err := wrapped.Call(context.Background(), nil, nil); err != nil {
t.Fatalf("metering error leaked into tool call: %v", err)
}
if calls != 1 {
t.Fatalf("delegate calls: want 1, got %d", calls)
}
}