mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-05-25 16:00:35 +00:00
795ee362ea
Remove the legacy EventKind/Event envelope mapping and let agent event emission build pkg/events.Event values directly. Keep HookMeta as the shared hook metadata shape and preserve legacy observe string aliases by mapping them to runtime event kinds. Validation: GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/agent; make lint
192 lines
5.9 KiB
Go
192 lines
5.9 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
|
|
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
runtimeevents "github.com/sipeed/picoclaw/pkg/events"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
)
|
|
|
|
func (al *AgentLoop) newTurnEventScope(agentID, sessionKey string, turnCtx *TurnContext) turnEventScope {
|
|
seq := al.turnSeq.Add(1)
|
|
return turnEventScope{
|
|
agentID: agentID,
|
|
sessionKey: sessionKey,
|
|
turnID: fmt.Sprintf("%s-turn-%d", agentID, seq),
|
|
context: cloneTurnContext(turnCtx),
|
|
}
|
|
}
|
|
|
|
func (ts turnEventScope) meta(iteration int, source, tracePath string) HookMeta {
|
|
return HookMeta{
|
|
AgentID: ts.agentID,
|
|
TurnID: ts.turnID,
|
|
SessionKey: ts.sessionKey,
|
|
Iteration: iteration,
|
|
Source: source,
|
|
TracePath: tracePath,
|
|
turnContext: cloneTurnContext(ts.context),
|
|
}
|
|
}
|
|
|
|
func (al *AgentLoop) emitEvent(kind runtimeevents.Kind, meta HookMeta, payload any) {
|
|
clonedMeta := cloneHookMeta(meta)
|
|
eventCtx := cloneTurnContext(clonedMeta.turnContext)
|
|
evt := runtimeevents.Event{
|
|
Kind: kind,
|
|
Source: runtimeevents.Source{Component: "agent", Name: clonedMeta.AgentID},
|
|
Scope: runtimeScopeFromHookMeta(clonedMeta, eventCtx),
|
|
Correlation: runtimeCorrelationFromHookMeta(clonedMeta),
|
|
Severity: runtimeSeverityForAgentEvent(kind, payload),
|
|
Payload: payload,
|
|
Attrs: runtimeAttrsFromHookMeta(clonedMeta),
|
|
}
|
|
|
|
if al == nil {
|
|
return
|
|
}
|
|
|
|
al.logEvent(evt, clonedMeta, eventCtx)
|
|
|
|
al.publishRuntimeEvent(evt)
|
|
}
|
|
|
|
func (al *AgentLoop) logEvent(evt runtimeevents.Event, meta HookMeta, eventCtx *TurnContext) {
|
|
fields := map[string]any{
|
|
"event_kind": evt.Kind.String(),
|
|
"agent_id": meta.AgentID,
|
|
"turn_id": meta.TurnID,
|
|
"session_key": meta.SessionKey,
|
|
"iteration": meta.Iteration,
|
|
}
|
|
|
|
if meta.TracePath != "" {
|
|
fields["trace"] = meta.TracePath
|
|
}
|
|
if meta.Source != "" {
|
|
fields["source"] = meta.Source
|
|
}
|
|
|
|
appendEventContextFields(fields, eventCtx)
|
|
|
|
switch payload := evt.Payload.(type) {
|
|
case TurnStartPayload:
|
|
fields["user_len"] = len(payload.UserMessage)
|
|
fields["media_count"] = payload.MediaCount
|
|
case TurnEndPayload:
|
|
fields["status"] = payload.Status
|
|
fields["iterations_total"] = payload.Iterations
|
|
fields["duration_ms"] = payload.Duration.Milliseconds()
|
|
fields["final_len"] = payload.FinalContentLen
|
|
case LLMRequestPayload:
|
|
fields["model"] = payload.Model
|
|
fields["messages"] = payload.MessagesCount
|
|
fields["tools"] = payload.ToolsCount
|
|
fields["max_tokens"] = payload.MaxTokens
|
|
case LLMDeltaPayload:
|
|
fields["content_delta_len"] = payload.ContentDeltaLen
|
|
fields["reasoning_delta_len"] = payload.ReasoningDeltaLen
|
|
case LLMResponsePayload:
|
|
fields["content_len"] = payload.ContentLen
|
|
fields["tool_calls"] = payload.ToolCalls
|
|
fields["has_reasoning"] = payload.HasReasoning
|
|
case LLMRetryPayload:
|
|
fields["attempt"] = payload.Attempt
|
|
fields["max_retries"] = payload.MaxRetries
|
|
fields["reason"] = payload.Reason
|
|
fields["error"] = payload.Error
|
|
fields["backoff_ms"] = payload.Backoff.Milliseconds()
|
|
case ContextCompressPayload:
|
|
fields["reason"] = payload.Reason
|
|
fields["dropped_messages"] = payload.DroppedMessages
|
|
fields["remaining_messages"] = payload.RemainingMessages
|
|
case SessionSummarizePayload:
|
|
fields["summarized_messages"] = payload.SummarizedMessages
|
|
fields["kept_messages"] = payload.KeptMessages
|
|
fields["summary_len"] = payload.SummaryLen
|
|
fields["omitted_oversized"] = payload.OmittedOversized
|
|
case ToolExecStartPayload:
|
|
fields["tool"] = payload.Tool
|
|
fields["args_count"] = len(payload.Arguments)
|
|
case ToolExecEndPayload:
|
|
fields["tool"] = payload.Tool
|
|
fields["duration_ms"] = payload.Duration.Milliseconds()
|
|
fields["for_llm_len"] = payload.ForLLMLen
|
|
fields["for_user_len"] = payload.ForUserLen
|
|
fields["is_error"] = payload.IsError
|
|
fields["async"] = payload.Async
|
|
case ToolExecSkippedPayload:
|
|
fields["tool"] = payload.Tool
|
|
fields["reason"] = payload.Reason
|
|
case SteeringInjectedPayload:
|
|
fields["count"] = payload.Count
|
|
fields["total_content_len"] = payload.TotalContentLen
|
|
case FollowUpQueuedPayload:
|
|
fields["source_tool"] = payload.SourceTool
|
|
fields["content_len"] = payload.ContentLen
|
|
case InterruptReceivedPayload:
|
|
fields["interrupt_kind"] = payload.Kind
|
|
fields["role"] = payload.Role
|
|
fields["content_len"] = payload.ContentLen
|
|
fields["queue_depth"] = payload.QueueDepth
|
|
fields["hint_len"] = payload.HintLen
|
|
case SubTurnSpawnPayload:
|
|
fields["child_agent_id"] = payload.AgentID
|
|
fields["label"] = payload.Label
|
|
case SubTurnEndPayload:
|
|
fields["child_agent_id"] = payload.AgentID
|
|
fields["status"] = payload.Status
|
|
case SubTurnResultDeliveredPayload:
|
|
fields["target_channel"] = payload.TargetChannel
|
|
fields["target_chat_id"] = payload.TargetChatID
|
|
fields["content_len"] = payload.ContentLen
|
|
case ErrorPayload:
|
|
fields["stage"] = payload.Stage
|
|
fields["error"] = payload.Message
|
|
}
|
|
|
|
logger.InfoCF("eventbus", fmt.Sprintf("Agent event: %s", evt.Kind.String()), fields)
|
|
}
|
|
|
|
// MountHook registers an in-process hook on the agent loop.
|
|
func (al *AgentLoop) MountHook(reg HookRegistration) error {
|
|
if al == nil || al.hooks == nil {
|
|
return fmt.Errorf("hook manager is not initialized")
|
|
}
|
|
return al.hooks.Mount(reg)
|
|
}
|
|
|
|
// UnmountHook removes a previously registered in-process hook.
|
|
func (al *AgentLoop) UnmountHook(name string) {
|
|
if al == nil || al.hooks == nil {
|
|
return
|
|
}
|
|
al.hooks.Unmount(name)
|
|
}
|
|
|
|
// RuntimeEvents returns the root runtime event channel.
|
|
func (al *AgentLoop) RuntimeEvents() runtimeevents.EventChannel {
|
|
if al == nil || al.runtimeEvents == nil {
|
|
return nil
|
|
}
|
|
return al.runtimeEvents.Channel()
|
|
}
|
|
|
|
// RuntimeEventStats returns runtime event bus counters.
|
|
func (al *AgentLoop) RuntimeEventStats() runtimeevents.Stats {
|
|
if al == nil || al.runtimeEvents == nil {
|
|
return runtimeevents.Stats{Closed: true}
|
|
}
|
|
return al.runtimeEvents.Stats()
|
|
}
|
|
|
|
// RuntimeEventBus returns the runtime event bus used by the agent loop.
|
|
func (al *AgentLoop) RuntimeEventBus() runtimeevents.Bus {
|
|
if al == nil {
|
|
return nil
|
|
}
|
|
return al.runtimeEvents
|
|
}
|