mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-16 14:23:34 +00:00
Add sessionKey to AgentEvent and filter in chat UI so events from Telegram, cron, and other channels don't leak into WS chat page.
197 lines
6.4 KiB
Go
197 lines
6.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tracing"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
// Run processes a single message through the agent loop.
|
|
// It blocks until completion and returns the final response.
|
|
func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
|
l.activeRuns.Add(1)
|
|
defer l.activeRuns.Add(-1)
|
|
|
|
// Per-run emit wrapper: enriches every AgentEvent with delegation + routing context.
|
|
emitRun := func(event AgentEvent) {
|
|
event.RunKind = req.RunKind
|
|
event.DelegationID = req.DelegationID
|
|
event.TeamID = req.TeamID
|
|
event.TeamTaskID = req.TeamTaskID
|
|
event.ParentAgentID = req.ParentAgentID
|
|
event.UserID = req.UserID
|
|
event.Channel = req.Channel
|
|
event.ChatID = req.ChatID
|
|
event.SessionKey = req.SessionKey
|
|
event.TenantID = store.TenantIDFromContext(ctx)
|
|
l.emit(event)
|
|
}
|
|
|
|
emitRun(AgentEvent{
|
|
Type: protocol.AgentEventRunStarted,
|
|
AgentID: l.id,
|
|
RunID: req.RunID,
|
|
Payload: map[string]any{"message": req.Message},
|
|
})
|
|
|
|
// Create trace
|
|
var traceID uuid.UUID
|
|
isChildTrace := req.ParentTraceID != uuid.Nil && l.traceCollector != nil
|
|
|
|
// agentSpanID holds the pre-generated root agent span ID.
|
|
// Used by emitAgentSpanEnd in the deferred finalizer below.
|
|
var agentSpanID uuid.UUID
|
|
|
|
if isChildTrace {
|
|
// Announce run: reuse parent trace, don't create new trace record.
|
|
// Spans will be added to the parent trace with proper nesting.
|
|
traceID = req.ParentTraceID
|
|
ctx = tracing.WithTraceID(ctx, traceID)
|
|
ctx = tracing.WithCollector(ctx, l.traceCollector)
|
|
agentSpanID = store.GenNewID()
|
|
ctx = tracing.WithParentSpanID(ctx, agentSpanID)
|
|
if req.ParentRootSpanID != uuid.Nil {
|
|
ctx = tracing.WithAnnounceParentSpanID(ctx, req.ParentRootSpanID)
|
|
}
|
|
} else if l.traceCollector != nil {
|
|
traceID = store.GenNewID()
|
|
now := time.Now().UTC()
|
|
traceName := "chat " + l.id
|
|
if req.TraceName != "" {
|
|
traceName = req.TraceName
|
|
}
|
|
trace := &store.TraceData{
|
|
ID: traceID,
|
|
RunID: req.RunID,
|
|
SessionKey: req.SessionKey,
|
|
UserID: req.UserID,
|
|
Channel: req.Channel,
|
|
Name: traceName,
|
|
InputPreview: truncateStr(req.Message, l.traceCollector.PreviewMaxLen()),
|
|
Status: store.TraceStatusRunning,
|
|
StartTime: now,
|
|
CreatedAt: now,
|
|
Tags: req.TraceTags,
|
|
}
|
|
if l.agentUUID != uuid.Nil {
|
|
trace.AgentID = &l.agentUUID
|
|
}
|
|
// Link to parent trace: delegation context or explicit LinkedTraceID (team task runs).
|
|
if delegateParent := tracing.DelegateParentTraceIDFromContext(ctx); delegateParent != uuid.Nil {
|
|
trace.ParentTraceID = &delegateParent
|
|
} else if req.LinkedTraceID != uuid.Nil {
|
|
trace.ParentTraceID = &req.LinkedTraceID
|
|
}
|
|
// Set team_id on trace for team-scoped runs.
|
|
if req.TeamID != "" {
|
|
if tid, err := uuid.Parse(req.TeamID); err == nil {
|
|
trace.TeamID = &tid
|
|
}
|
|
}
|
|
if err := l.traceCollector.CreateTrace(ctx, trace); err != nil {
|
|
slog.Warn("tracing: failed to create trace", "error", err)
|
|
} else {
|
|
ctx = tracing.WithTraceID(ctx, traceID)
|
|
ctx = tracing.WithCollector(ctx, l.traceCollector)
|
|
if trace.TeamID != nil {
|
|
ctx = tracing.WithTraceTeamID(ctx, *trace.TeamID)
|
|
}
|
|
|
|
// Pre-generate root "agent" span ID so LLM/tool spans can reference it as parent.
|
|
agentSpanID = store.GenNewID()
|
|
ctx = tracing.WithParentSpanID(ctx, agentSpanID)
|
|
}
|
|
}
|
|
|
|
// Inject local key into tool context so delegation/subagent tools can
|
|
// propagate topic/thread routing info back through announce messages.
|
|
if req.LocalKey != "" {
|
|
ctx = tools.WithToolLocalKey(ctx, req.LocalKey)
|
|
}
|
|
|
|
runStart := time.Now().UTC()
|
|
|
|
// Emit running agent span immediately so it's visible in the trace UI.
|
|
if agentSpanID != uuid.Nil {
|
|
l.emitAgentSpanStart(ctx, agentSpanID, runStart, req.Message)
|
|
}
|
|
|
|
// Child trace (announce run): set parent trace back to "running" while
|
|
// this run is active so the trace UI doesn't show "completed" with a
|
|
// "running" child span.
|
|
if isChildTrace && l.traceCollector != nil && traceID != uuid.Nil {
|
|
l.traceCollector.SetTraceStatus(ctx, traceID, store.TraceStatusRunning)
|
|
}
|
|
|
|
result, err := l.runLoop(ctx, req)
|
|
|
|
// Finalize the root agent span. Uses EmitSpanUpdate (channel send) so it
|
|
// succeeds even if ctx is cancelled. Must run before FinishTrace so
|
|
// aggregates include this span.
|
|
if agentSpanID != uuid.Nil {
|
|
l.emitAgentSpanEnd(ctx, agentSpanID, runStart, result, err)
|
|
}
|
|
|
|
// Child trace: restore trace status now that this run is done.
|
|
if isChildTrace && l.traceCollector != nil && traceID != uuid.Nil {
|
|
status := store.TraceStatusCompleted
|
|
if err != nil {
|
|
status = store.TraceStatusError
|
|
}
|
|
l.traceCollector.SetTraceStatus(ctx, traceID, status)
|
|
}
|
|
|
|
if err != nil {
|
|
emitRun(AgentEvent{
|
|
Type: protocol.AgentEventRunFailed,
|
|
AgentID: l.id,
|
|
RunID: req.RunID,
|
|
Payload: map[string]string{"error": err.Error()},
|
|
})
|
|
// Only finish trace for root runs; child traces don't own the trace lifecycle.
|
|
// Use background context when the run context is cancelled (/stop command)
|
|
// so the DB update still succeeds.
|
|
if !isChildTrace && l.traceCollector != nil && traceID != uuid.Nil {
|
|
traceCtx := ctx
|
|
traceStatus := store.TraceStatusError
|
|
if ctx.Err() != nil {
|
|
traceCtx = context.WithoutCancel(ctx)
|
|
traceStatus = store.TraceStatusCancelled
|
|
}
|
|
l.traceCollector.FinishTrace(traceCtx, traceID, traceStatus, err.Error(), "")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
completedPayload := map[string]any{"content": result.Content}
|
|
if result.Usage != nil {
|
|
completedPayload["usage"] = map[string]any{
|
|
"prompt_tokens": result.Usage.PromptTokens,
|
|
"completion_tokens": result.Usage.CompletionTokens,
|
|
"total_tokens": result.Usage.TotalTokens,
|
|
"cache_creation_tokens": result.Usage.CacheCreationTokens,
|
|
"cache_read_tokens": result.Usage.CacheReadTokens,
|
|
}
|
|
}
|
|
if len(result.Media) > 0 {
|
|
completedPayload["media"] = result.Media
|
|
}
|
|
emitRun(AgentEvent{
|
|
Type: protocol.AgentEventRunCompleted,
|
|
AgentID: l.id,
|
|
RunID: req.RunID,
|
|
Payload: completedPayload,
|
|
})
|
|
if !isChildTrace && l.traceCollector != nil && traceID != uuid.Nil {
|
|
l.traceCollector.FinishTrace(ctx, traceID, store.TraceStatusCompleted, "", truncateStr(result.Content, l.traceCollector.PreviewMaxLen()))
|
|
}
|
|
return result, nil
|
|
}
|