mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-11 02:10:51 +00:00
9a9744077e
Major refactoring of the team system with multiple improvements:
## Removed legacy delegation tools
- Delete `delegate.go`, `delegate_async.go`, `delegate_sync.go`, `delegate_events.go`,
`delegate_policy.go`, `delegate_prep.go`, `delegate_state.go`, `delegate_search_tool.go`
- Delete `evaluate_loop_tool.go`, `handoff_tool.go`
- Remove all references and registrations from tool manager and policy
- Clean up TEAM_PLAYBOOK_IDEAS.md and TEAM_SYSTEM.md (moved to docs)
## Rename await_reply → ask_user
- Rename action `await_reply` → `ask_user`, `clear_followup` → `clear_ask_user`
- Rename functions `executeAwaitReply` → `executeAskUser`, `executeClearFollowup` → `executeClearAskUser`
- Update system prompt with stronger wording to prevent model misuse
- Model was confusing "await_reply" with general waiting; "ask_user" is unambiguous
## Fix auto-followup false positives
- Add `HasActiveMemberTasks(ctx, teamID, excludeAgentID)` store method
- Guard `autoSetFollowup()` in consumer: skip when lead has active member tasks
- Prevents auto-followup when lead is orchestrating teammates (not waiting for user)
## Task identifier zero-padding
- Change format from `T-1-xxxx` → `T-001-xxxx` (3-digit minimum)
## Refactor workspace WS handlers to filesystem-only
- Rewrite `teams.workspace.list/read/delete` to use pure filesystem (os.ReadDir/ReadFile/Remove)
- Remove DB dependency from workspace WS handlers
- Consistent with storage handler and workspace tools
- Simplify TeamWorkspaceFile type and frontend hook
## Add team events listing API
- New WS method `teams.events.list` with team_id, limit, offset params
- New HTTP endpoint `GET /v1/teams/{id}/events` with bearer auth
- New `ListTeamEvents(ctx, teamID, limit, offset)` store method
- JOIN with team_tasks for team-wide event filtering
## Extract team access policy
- New `team_access_policy.go` — centralized team tool access control
## Migration 000019: team_id columns
- Add team_id foreign key columns to relevant tables
## Other improvements
- Add team_id propagation through agent loop, tracing, sessions
- Update i18n locale files (en/vi/zh) for new tool labels
- Update frontend builtin-tools page and require-setup component
- Bump RequiredSchemaVersion for migration 000019
192 lines
6.2 KiB
Go
192 lines
6.2 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
|
|
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.Background()
|
|
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,
|
|
}
|
|
}
|
|
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
|
|
}
|