first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
// This file implements structured telemetry collection for a loop run
// (observability -- structured telemetry collection). A lightweight accumulator observes the AgentEvents the loop
// already emits and folds them into a compact summary — per-tool wall-clock
// durations, turn count, truncation count, compaction count, and the latest
// context-utilization ratio — that is surfaced once at run end as a
// TelemetryEvent (just before agent_end).
//
// The design is deliberately additive: telemetry rides the existing AgentEvent
// family and the existing stream-json output path, so no new dependency
// (Prometheus/OTLP) is introduced and existing stream-json consumers that do
// not know the "telemetry" event type keep working unchanged. Collection is
// passive — observing an event never changes loop behavior.
package runtime
import (
"sync"
"time"
"github.com/smallnest/pigo/internal/agentcore"
)
// telemetry is the per-run accumulator. Most fields are folded from events on
// the single runLoop goroutine, but tool_execution_* events fire from the
// parallel tool-batch goroutines (ExecuteToolCalls), so all mutation goes
// through mu.
type telemetry struct {
mu sync.Mutex
turns int
truncationCount int
compactionCount int
// toolStarts maps an in-flight tool call id to the wall-clock time its
// execution began, so the matching end event can compute a duration. Keying
// by call id (not tool name) keeps parallel tool batches correct.
toolStarts map[string]time.Time
// toolTimings aggregates finished tool durations by tool name.
toolTimings map[string]agentcore.ToolTiming
// contextTokens / contextWindow capture the most recent context accounting so
// the summary can report the latest utilization ratio. contextWindow == 0
// means the window is unknown (utilization is then reported as 0).
contextTokens int
contextWindow int
// now is the clock, injectable for deterministic tests. Defaults to
// time.Now.
now func() time.Time
}
// newTelemetry constructs an empty accumulator using the wall clock.
func newTelemetry() *telemetry {
return &telemetry{
toolStarts: make(map[string]time.Time),
toolTimings: make(map[string]agentcore.ToolTiming),
now: time.Now,
}
}
// observe folds a single emitted event into the accumulator. It is a no-op for
// event types that carry no telemetry signal, and it never mutates the event.
func (t *telemetry) observe(ev agentcore.AgentEvent) {
if t == nil {
return
}
t.mu.Lock()
defer t.mu.Unlock()
switch e := ev.(type) {
case agentcore.TurnStartEvent:
t.turns++
case agentcore.ToolExecutionStartEvent:
t.toolStarts[e.ToolCallID] = t.now()
case agentcore.ToolExecutionEndEvent:
start, ok := t.toolStarts[e.ToolCallID]
if !ok {
return
}
delete(t.toolStarts, e.ToolCallID)
elapsed := t.now().Sub(start).Milliseconds()
if elapsed < 0 {
elapsed = 0
}
agg := t.toolTimings[e.ToolName]
agg.Count++
agg.TotalMs += elapsed
t.toolTimings[e.ToolName] = agg
case agentcore.TurnEndEvent:
if e.Message.StopReason == agentcore.StopReasonLength {
t.truncationCount++
}
case agentcore.CompactionEvent:
// Count only successful compactions; a failed one (ErrorMessage set) left
// the context unchanged.
if e.ErrorMessage == "" {
t.compactionCount++
}
}
}
// recordContext captures the latest context-token usage and window so the
// summary can report the current utilization ratio. A non-positive window is
// treated as unknown.
func (t *telemetry) recordContext(tokens, window int) {
if t == nil {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.contextTokens = tokens
if window > 0 {
t.contextWindow = window
}
}
// summary materializes the accumulated metrics into a TelemetryEvent. The
// per-tool map is copied so the emitted event does not alias the accumulator's
// live state.
func (t *telemetry) summary() agentcore.TelemetryEvent {
t.mu.Lock()
defer t.mu.Unlock()
timings := make(map[string]agentcore.ToolTiming, len(t.toolTimings))
for name, v := range t.toolTimings {
timings[name] = v
}
var utilization float64
if t.contextWindow > 0 {
utilization = float64(t.contextTokens) / float64(t.contextWindow)
}
return agentcore.TelemetryEvent{
Turns: t.turns,
ToolDurationsMs: timings,
TruncationCount: t.truncationCount,
CompactionCount: t.compactionCount,
ContextUtilization: utilization,
ContextTokens: t.contextTokens,
ContextWindow: t.contextWindow,
}
}