First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"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"
|
||||
)
|
||||
|
||||
// maxLedgerSkillName caps the skill name stored for a MISS — that string comes
|
||||
// straight from the model and is otherwise unbounded.
|
||||
const maxLedgerSkillName = 128
|
||||
|
||||
// meteredSkill wraps the Skill meta-tool to append one skill_usage row per call.
|
||||
// The metering lives here rather than in Registry.OnInvoke because OnInvoke only
|
||||
// receives the resolved Skill: it never fires for an unknown skill name (norma
|
||||
// returns a tool error before the hook) and it cannot see the caller's args. Both
|
||||
// matter — a miss tells you which procedure the agents wished existed.
|
||||
type meteredSkill struct {
|
||||
actool.CoreTool
|
||||
pg *db.DB
|
||||
reg *skill.Registry
|
||||
agentKey string
|
||||
ri agent.RunInfo
|
||||
}
|
||||
|
||||
// meterSkillTool returns t unchanged when there is no DB to meter into.
|
||||
func meterSkillTool(t actool.CoreTool, pg *db.DB, reg *skill.Registry, agentKey string, ri agent.RunInfo) actool.CoreTool {
|
||||
if pg == nil {
|
||||
return t
|
||||
}
|
||||
return &meteredSkill{CoreTool: t, pg: pg, reg: reg, agentKey: agentKey, ri: ri}
|
||||
}
|
||||
|
||||
func (m *meteredSkill) Call(ctx context.Context, input json.RawMessage, tc *actool.ToolContext) (actool.Result, error) {
|
||||
m.record(input)
|
||||
return m.CoreTool.Call(ctx, input, tc)
|
||||
}
|
||||
|
||||
// record appends the ledger row. Best-effort by design: a metering failure must
|
||||
// never turn into a failed skill invocation, so every error path just logs.
|
||||
func (m *meteredSkill) record(input json.RawMessage) {
|
||||
var in struct {
|
||||
Name string `json:"name"`
|
||||
Args string `json:"args"`
|
||||
}
|
||||
if err := json.Unmarshal(input, &in); err != nil {
|
||||
return // malformed call; norma will reject it too
|
||||
}
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
// Resolve to the directory name so the ledger keys match agent_skill_visibility
|
||||
// and the skills page (a skill's display Name can differ from its directory).
|
||||
found := false
|
||||
if m.reg != nil {
|
||||
if s, ok := m.reg.Get(name); ok {
|
||||
found = true
|
||||
if s.Dir != "" {
|
||||
name = filepath.Base(s.Dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(name) > maxLedgerSkillName {
|
||||
name = name[:maxLedgerSkillName]
|
||||
}
|
||||
err := m.pg.InsertSkillUsage(&db.SkillUsage{
|
||||
Skill: name,
|
||||
AgentKey: m.agentKey,
|
||||
TaskID: m.ri.TaskID,
|
||||
ExplorationID: m.ri.ExplorationID,
|
||||
IntentID: m.ri.IntentID,
|
||||
SessionID: m.ri.SessionID,
|
||||
ArgsLen: len(in.Args),
|
||||
Found: found,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[skillusage] insert: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user