mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-27 12:19:51 +00:00
201 lines
5.5 KiB
Go
201 lines
5.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tracing"
|
|
)
|
|
|
|
type mcpUsageTool interface {
|
|
ServerName() string
|
|
OriginalName() string
|
|
}
|
|
|
|
func (l *Loop) canonicalToolName(name string) string {
|
|
if l.registry == nil {
|
|
return name
|
|
}
|
|
if tool, ok := l.registry.Get(name); ok && tool != nil {
|
|
return tool.Name()
|
|
}
|
|
return name
|
|
}
|
|
|
|
func (l *Loop) recordToolUsageEvent(ctx context.Context, req *RunRequest, canonicalName, rawName, toolCallID string, args map[string]any, start time.Time, result *tools.Result, spanID uuid.UUID) {
|
|
if l.usageEvents == nil || result == nil {
|
|
return
|
|
}
|
|
if tracing.TraceIDFromContext(ctx) == uuid.Nil {
|
|
return
|
|
}
|
|
|
|
resourceName := canonicalName
|
|
resourceID := canonicalName
|
|
eventType := store.UsageEventTypeToolCall
|
|
resourceType := store.UsageResourceTypeTool
|
|
source := store.UsageSourceToolCall
|
|
metadata := map[string]any{}
|
|
|
|
if canonicalName == "use_skill" {
|
|
eventType = store.UsageEventTypeSkillActivation
|
|
resourceType = store.UsageResourceTypeSkill
|
|
source = store.UsageSourceUseSkill
|
|
if skill, _ := args["name"].(string); skill != "" {
|
|
resourceName = skill
|
|
resourceID = skill
|
|
}
|
|
} else if l.registry != nil {
|
|
tool, ok := l.registry.Get(canonicalName)
|
|
if !ok {
|
|
tool = nil
|
|
}
|
|
if mcpTool, ok := tool.(mcpUsageTool); ok {
|
|
eventType = store.UsageEventTypeMCPToolCall
|
|
resourceType = store.UsageResourceTypeMCPTool
|
|
resourceName = mcpTool.ServerName() + "/" + mcpTool.OriginalName()
|
|
resourceID = mcpTool.OriginalName()
|
|
metadata["server"] = mcpTool.ServerName()
|
|
metadata["tool"] = mcpTool.OriginalName()
|
|
} else if l.isRuntimeTool(canonicalName) {
|
|
eventType = store.UsageEventTypeRuntimeToolCall
|
|
resourceType = store.UsageResourceTypeRuntimeTool
|
|
}
|
|
}
|
|
|
|
if rawName != "" && rawName != canonicalName {
|
|
metadata["raw_tool_name"] = rawName
|
|
}
|
|
|
|
event := l.baseUsageEvent(ctx, req, start, eventType, resourceType, resourceName, resourceID, source)
|
|
event.SpanID = uuidPtr(spanID)
|
|
event.Status = "completed"
|
|
if result.IsError {
|
|
event.Status = "error"
|
|
event.ErrorCount = 1
|
|
}
|
|
event.DurationMS = int(time.Since(start).Milliseconds())
|
|
if result.Usage != nil {
|
|
event.InputTokens = int64(result.Usage.PromptTokens)
|
|
event.OutputTokens = int64(result.Usage.CompletionTokens)
|
|
event.TotalTokens = int64(result.Usage.TotalTokens)
|
|
if event.TotalTokens == 0 {
|
|
event.TotalTokens = event.InputTokens + event.OutputTokens
|
|
}
|
|
}
|
|
event.Provider = result.Provider
|
|
event.Model = result.Model
|
|
event.Metadata = usageMetadata(metadata)
|
|
l.insertUsageEventBestEffort(ctx, event)
|
|
}
|
|
|
|
func (l *Loop) recordSkillSlashUsageEvent(ctx context.Context, skillSlug string) {
|
|
if l.usageEvents == nil || skillSlug == "" {
|
|
return
|
|
}
|
|
traceID := tracing.TraceIDFromContext(ctx)
|
|
if traceID == uuid.Nil {
|
|
return
|
|
}
|
|
rc := store.RunContextFromCtx(ctx)
|
|
event := l.baseUsageEvent(ctx, nil, time.Now().UTC(),
|
|
store.UsageEventTypeSkillActivation,
|
|
store.UsageResourceTypeSkill,
|
|
skillSlug,
|
|
skillSlug,
|
|
store.UsageSourceSlashCommand,
|
|
)
|
|
event.TraceID = uuidPtr(traceID)
|
|
if rc != nil {
|
|
event.RunID = rc.RunID
|
|
event.SessionKey = rc.SessionKey
|
|
event.Channel = rc.Channel
|
|
if rc.TeamID != "" {
|
|
if teamID, err := uuid.Parse(rc.TeamID); err == nil {
|
|
event.TeamID = &teamID
|
|
}
|
|
}
|
|
}
|
|
event.Metadata = usageMetadata(map[string]any{"activation_source": store.UsageSourceSlashCommand})
|
|
l.insertUsageEventBestEffort(ctx, event)
|
|
}
|
|
|
|
func (l *Loop) baseUsageEvent(ctx context.Context, req *RunRequest, eventTime time.Time, eventType, resourceType, resourceName, resourceID, source string) store.UsageEvent {
|
|
tenantID := store.TenantIDFromContext(ctx)
|
|
if tenantID == uuid.Nil {
|
|
tenantID = l.tenantID
|
|
}
|
|
traceID := tracing.TraceIDFromContext(ctx)
|
|
event := store.UsageEvent{
|
|
ID: uuid.New(),
|
|
TenantID: tenantID,
|
|
EventTime: eventTime.UTC(),
|
|
BucketHour: eventTime.UTC().Truncate(time.Hour),
|
|
EventType: eventType,
|
|
ResourceType: resourceType,
|
|
ResourceName: resourceName,
|
|
ResourceID: resourceID,
|
|
Source: source,
|
|
AgentID: uuidPtr(l.agentUUID),
|
|
TeamID: tracing.TraceTeamIDPtrFromContext(ctx),
|
|
TraceID: uuidPtr(traceID),
|
|
Status: "completed",
|
|
CallCount: 1,
|
|
}
|
|
if req != nil {
|
|
event.RunID = req.RunID
|
|
event.SessionKey = req.SessionKey
|
|
event.Channel = req.Channel
|
|
if req.TeamID != "" && event.TeamID == nil {
|
|
if teamID, err := uuid.Parse(req.TeamID); err == nil {
|
|
event.TeamID = &teamID
|
|
}
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
|
|
func (l *Loop) isRuntimeTool(toolName string) bool {
|
|
if l.registry == nil {
|
|
return false
|
|
}
|
|
members, ok := l.registry.GetToolGroup("runtime")
|
|
return ok && slices.Contains(members, toolName)
|
|
}
|
|
|
|
func (l *Loop) insertUsageEventBestEffort(ctx context.Context, event store.UsageEvent) {
|
|
tenantID := event.TenantID
|
|
go func() {
|
|
bgCtx, cancel := context.WithTimeout(store.WithTenantID(context.Background(), tenantID), 5*time.Second)
|
|
defer cancel()
|
|
if err := l.usageEvents.InsertEvent(bgCtx, &event); err != nil {
|
|
slog.Debug("usage.event.record_failed", "resource", event.ResourceName, "error", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func uuidPtr(id uuid.UUID) *uuid.UUID {
|
|
if id == uuid.Nil {
|
|
return nil
|
|
}
|
|
return &id
|
|
}
|
|
|
|
func usageMetadata(values map[string]any) json.RawMessage {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
data, err := json.Marshal(values)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return data
|
|
}
|