feat: complete agent monitoring - hook, UI, and backend filter
- Add event_type and framework filters to events query endpoint - Add /agents SPA route to web-ui server - Add Agents nav link and route in frontend - Add agents page CSS (timeline, VM pills, stats panel) - Build VM status strip, activity timeline, and real-time stats - Add agentmon hook for OpenClaw (HOOK.md + handler.ts) - Add docker-compose, Dockerfile, and supporting infra files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewSessionStart creates a session.start event.
|
||||
func NewSessionStart(sessionID string, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "session.start",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// NewSessionEnd creates a session.end event.
|
||||
func NewSessionEnd(sessionID string, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "session.end",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// NewRunStart creates a run.start event.
|
||||
func NewRunStart(sessionID, runID string, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "run.start",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
"run_id": runID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// NewRunEnd creates a run.end event.
|
||||
func NewRunEnd(sessionID, runID string, status string, durationMs int64, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "run.end",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
"run_id": runID,
|
||||
},
|
||||
"payload": map[string]any{
|
||||
"status": status,
|
||||
"duration_ms": durationMs,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// NewSpanStart creates a span.start event.
|
||||
func NewSpanStart(sessionID, runID, traceID, spanID string, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "span.start",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
"run_id": runID,
|
||||
"trace_id": traceID,
|
||||
"span_id": spanID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// NewSpanEnd creates a span.end event.
|
||||
func NewSpanEnd(sessionID, runID, traceID, spanID string, status string, durationMs int64, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "span.end",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
"run_id": runID,
|
||||
"trace_id": traceID,
|
||||
"span_id": spanID,
|
||||
},
|
||||
"payload": map[string]any{
|
||||
"status": status,
|
||||
"duration_ms": durationMs,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// NewError creates an error event.
|
||||
func NewError(sessionID, runID, traceID, spanID string, errType, message string, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "error",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
"run_id": runID,
|
||||
"trace_id": traceID,
|
||||
"span_id": spanID,
|
||||
},
|
||||
"payload": map[string]any{
|
||||
"error": map[string]any{
|
||||
"type": errType,
|
||||
"message": message,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// NewMetricSnapshot creates a metric.snapshot event.
|
||||
func NewMetricSnapshot(sessionID, runID string, metrics map[string]any, opts ...EventOption) Event {
|
||||
now := time.Now()
|
||||
event := map[string]any{
|
||||
"schema": map[string]any{
|
||||
"name": schemaName,
|
||||
"version": schemaVersion,
|
||||
},
|
||||
"event": map[string]any{
|
||||
"id": generateID(),
|
||||
"type": "metric.snapshot",
|
||||
"ts": now.UTC().Format(time.RFC3339Nano),
|
||||
},
|
||||
"correlation": map[string]any{
|
||||
"session_id": sessionID,
|
||||
"run_id": runID,
|
||||
},
|
||||
"payload": map[string]any{
|
||||
"metrics": metrics,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(event)
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// EventOption is a function that modifies an event.
|
||||
type EventOption func(Event)
|
||||
|
||||
// WithSource sets the source information on an event.
|
||||
func WithSource(emitter *Emitter) EventOption {
|
||||
return func(e Event) {
|
||||
if event, ok := e["event"].(map[string]any); ok {
|
||||
event["source"] = map[string]any{
|
||||
"framework": emitter.config.Framework,
|
||||
"client_id": emitter.config.ClientID,
|
||||
"host": emitter.config.Host,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithAttributes adds attributes to an event.
|
||||
func WithAttributes(attrs map[string]any) EventOption {
|
||||
return func(e Event) {
|
||||
if _, ok := e["attributes"]; !ok {
|
||||
e["attributes"] = make(map[string]any)
|
||||
}
|
||||
if attrsMap, ok := e["attributes"].(map[string]any); ok {
|
||||
for k, v := range attrs {
|
||||
attrsMap[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithSpanKind sets the span_kind attribute.
|
||||
func WithSpanKind(kind string) EventOption {
|
||||
return WithAttributes(map[string]any{"span_kind": kind})
|
||||
}
|
||||
|
||||
// WithName sets the name attribute.
|
||||
func WithName(name string) EventOption {
|
||||
return WithAttributes(map[string]any{"name": name})
|
||||
}
|
||||
|
||||
// WithParentSpanID sets the parent_span_id in correlation.
|
||||
func WithParentSpanID(parentID string) EventOption {
|
||||
return func(e Event) {
|
||||
if corr, ok := e["correlation"].(map[string]any); ok {
|
||||
corr["parent_span_id"] = parentID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithPayload sets the payload on an event.
|
||||
func WithPayload(payload map[string]any) EventOption {
|
||||
return func(e Event) {
|
||||
e["payload"] = payload
|
||||
}
|
||||
}
|
||||
|
||||
// WithSeq sets the sequence number on an event.
|
||||
func WithSeq(seq int) EventOption {
|
||||
return func(e Event) {
|
||||
if event, ok := e["event"].(map[string]any); ok {
|
||||
event["seq"] = seq
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithLLMUsage adds LLM usage information to a span.end or run.end payload.
|
||||
func WithLLMUsage(model string, inputTokens, outputTokens int, costUSD float64) EventOption {
|
||||
return func(e Event) {
|
||||
if payload, ok := e["payload"].(map[string]any); ok {
|
||||
if _, ok := payload["llm"]; !ok {
|
||||
payload["llm"] = make(map[string]any)
|
||||
}
|
||||
if llm, ok := payload["llm"].(map[string]any); ok {
|
||||
llm["model"] = model
|
||||
llm["usage"] = map[string]any{
|
||||
"input_tokens": inputTokens,
|
||||
"output_tokens": outputTokens,
|
||||
}
|
||||
llm["cost"] = map[string]any{
|
||||
"total_usd": costUSD,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorDetails adds error details to a payload.
|
||||
func WithErrorDetails(code string, retryable bool) EventOption {
|
||||
return func(e Event) {
|
||||
if payload, ok := e["payload"].(map[string]any); ok {
|
||||
if err, ok := payload["error"].(map[string]any); ok {
|
||||
err["code"] = code
|
||||
err["retryable"] = retryable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user