Files
tutti/packages/agent/daemon/runtime/codex_appserver_turn_diagnostics.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

289 lines
8.2 KiB
Go

package agentruntime
import (
"strings"
"sync"
"time"
)
type codexAppServerTurnTraceSink interface {
Log(event string, fields map[string]any)
}
type codexAppServerToolTiming struct {
startedAt time.Time
itemType string
}
// codexAppServerTurnDiagnostics writes bounded lifecycle records to the
// exported app-server diagnostics trace. It intentionally excludes tool
// arguments, commands, paths, output, queries, URLs, and error text.
type codexAppServerTurnDiagnostics struct {
mu sync.Mutex
trace codexAppServerTurnTraceSink
now func() time.Time
startedAt time.Time
turnID string
providerTurnID string
finished bool
tools map[string]codexAppServerToolTiming
completedTools map[string]struct{}
startedCount int
completedCount int
failedCount int
maxDuration time.Duration
maxItemType string
}
func newCodexAppServerTurnDiagnostics(
trace codexAppServerTurnTraceSink,
turnID string,
) *codexAppServerTurnDiagnostics {
now := time.Now
diagnostics := &codexAppServerTurnDiagnostics{
now: now,
turnID: strings.TrimSpace(turnID),
tools: make(map[string]codexAppServerToolTiming),
completedTools: make(map[string]struct{}),
}
diagnostics.Start(trace)
return diagnostics
}
func (d *codexAppServerTurnDiagnostics) Start(trace codexAppServerTurnTraceSink) {
if d == nil || trace == nil {
return
}
d.mu.Lock()
defer d.mu.Unlock()
if d.trace != nil {
return
}
d.trace = trace
d.startedAt = d.now()
}
func (d *codexAppServerTurnDiagnostics) ObserveNotification(method string, params map[string]any) {
if d == nil {
return
}
switch method {
case appServerNotifyItemStarted:
d.toolStarted(payloadObject(params["item"]), asString(params["threadId"]), asString(params["turnId"]))
case appServerNotifyItemCompleted:
d.toolCompleted(payloadObject(params["item"]), asString(params["threadId"]), asString(params["turnId"]))
}
}
func (d *codexAppServerTurnDiagnostics) toolStarted(item map[string]any, providerThreadID, providerTurnID string) {
itemID, itemType, safeFields, ok := codexAppServerToolDiagnosticFields(item)
if !ok {
return
}
toolKey := codexAppServerToolDiagnosticKey(providerThreadID, itemID)
now := d.now()
d.mu.Lock()
if d.trace == nil || d.finished {
d.mu.Unlock()
return
}
if _, exists := d.tools[toolKey]; exists {
d.mu.Unlock()
return
}
if _, completed := d.completedTools[toolKey]; completed {
d.mu.Unlock()
return
}
d.tools[toolKey] = codexAppServerToolTiming{startedAt: now, itemType: itemType}
d.startedCount++
fields := d.toolFieldsLocked(itemID, itemType, providerThreadID, providerTurnID, safeFields)
trace := d.trace
d.mu.Unlock()
trace.Log("turn.tool.started", fields)
}
func (d *codexAppServerTurnDiagnostics) toolCompleted(item map[string]any, providerThreadID, providerTurnID string) {
itemID, itemType, safeFields, ok := codexAppServerToolDiagnosticFields(item)
if !ok {
return
}
toolKey := codexAppServerToolDiagnosticKey(providerThreadID, itemID)
now := d.now()
d.mu.Lock()
if d.trace == nil || d.finished {
d.mu.Unlock()
return
}
if _, completed := d.completedTools[toolKey]; completed {
d.mu.Unlock()
return
}
d.completedTools[toolKey] = struct{}{}
d.completedCount++
fields := d.toolFieldsLocked(itemID, itemType, providerThreadID, providerTurnID, safeFields)
if timing, started := d.tools[toolKey]; started {
duration := now.Sub(timing.startedAt)
if duration < 0 {
duration = 0
}
fields["duration_ms"] = duration.Milliseconds()
fields["start_observed"] = true
delete(d.tools, toolKey)
if duration > d.maxDuration {
d.maxDuration = duration
d.maxItemType = timing.itemType
}
} else {
fields["start_observed"] = false
}
outcome, failed := codexAppServerToolDiagnosticOutcome(item)
fields["outcome"] = outcome
if failed {
d.failedCount++
}
trace := d.trace
d.mu.Unlock()
trace.Log("turn.tool.completed", fields)
}
func (d *codexAppServerTurnDiagnostics) Finish(outcome, providerTurnID string) {
if d == nil {
return
}
now := d.now()
d.mu.Lock()
if d.trace == nil || d.finished {
d.mu.Unlock()
return
}
d.finished = true
if providerTurnID = strings.TrimSpace(providerTurnID); providerTurnID != "" {
d.providerTurnID = providerTurnID
}
fields := d.baseFieldsLocked()
duration := now.Sub(d.startedAt)
if duration < 0 {
duration = 0
}
fields["duration_ms"] = duration.Milliseconds()
fields["outcome"] = firstNonEmpty(strings.TrimSpace(outcome), "unknown")
fields["tool_started_count"] = d.startedCount
fields["tool_completed_count"] = d.completedCount
fields["tool_failed_count"] = d.failedCount
fields["open_tool_count"] = len(d.tools)
if d.maxDuration > 0 {
fields["max_tool_duration_ms"] = d.maxDuration.Milliseconds()
fields["max_tool_item_type"] = d.maxItemType
}
var longestOpen time.Duration
var longestOpenType string
for _, timing := range d.tools {
openFor := now.Sub(timing.startedAt)
if openFor > longestOpen {
longestOpen = openFor
longestOpenType = timing.itemType
}
}
if longestOpen > 0 {
fields["longest_open_tool_ms"] = longestOpen.Milliseconds()
fields["longest_open_item_type"] = longestOpenType
}
trace := d.trace
d.mu.Unlock()
trace.Log("turn.completed", fields)
}
func (d *codexAppServerTurnDiagnostics) baseFieldsLocked() map[string]any {
return map[string]any{
"turn_id": d.turnID,
"provider_turn_id": d.providerTurnID,
}
}
func (d *codexAppServerTurnDiagnostics) toolFieldsLocked(
itemID string,
itemType string,
providerThreadID string,
providerTurnID string,
safeFields map[string]any,
) map[string]any {
fields := d.baseFieldsLocked()
fields["item_id"] = itemID
fields["item_type"] = itemType
if providerThreadID = strings.TrimSpace(providerThreadID); providerThreadID != "" {
fields["item_provider_thread_id"] = providerThreadID
}
if providerTurnID = strings.TrimSpace(providerTurnID); providerTurnID != "" {
fields["item_provider_turn_id"] = providerTurnID
}
for key, value := range safeFields {
fields[key] = value
}
return fields
}
func codexAppServerToolDiagnosticKey(providerThreadID, itemID string) string {
return strings.TrimSpace(providerThreadID) + "\x00" + strings.TrimSpace(itemID)
}
func codexAppServerToolDiagnosticFields(item map[string]any) (string, string, map[string]any, bool) {
itemID := strings.TrimSpace(asString(item["id"]))
itemType := strings.TrimSpace(asString(item["type"]))
if itemID == "" {
return "", "", nil, false
}
fields := map[string]any{}
switch itemType {
case "commandExecution":
if output := asStringRaw(item["aggregatedOutput"]); output != "" {
fields["output_bytes"] = len(output)
}
if exitCode, ok := acpIntFromValue(item["exitCode"]); ok {
fields["exit_code"] = exitCode
}
case "fileChange":
if changes, ok := item["changes"].([]any); ok {
fields["change_count"] = len(changes)
}
case "mcpToolCall":
fields["tool_server"] = asString(item["server"])
fields["tool_name"] = asString(item["tool"])
fields["has_error"] = strings.TrimSpace(asStringRaw(item["error"])) != ""
case "webSearch":
action := payloadObject(item["action"])
fields["action_type"] = firstNonEmpty(asString(action["type"]), "search")
fields["query_count"] = len(appServerSearchQueries(action["queries"]))
fields["has_url"] = strings.TrimSpace(asString(action["url"])) != ""
case "dynamicToolCall", "collabAgentToolCall":
fields["tool_name"] = asString(item["tool"])
case "imageGeneration":
fields["has_saved_path"] = strings.TrimSpace(asString(item["savedPath"])) != ""
case "imageView":
default:
return "", "", nil, false
}
return itemID, itemType, fields, true
}
func codexAppServerToolDiagnosticOutcome(item map[string]any) (string, bool) {
status := firstNonEmpty(strings.TrimSpace(asString(item["status"])), "completed")
failed := status == "failed" || status == "declined" || status == "canceled" || status == "cancelled"
switch asString(item["type"]) {
case "commandExecution":
if exitCode, ok := acpIntFromValue(item["exitCode"]); ok && exitCode != 0 {
failed = true
}
case "mcpToolCall":
failed = failed || strings.TrimSpace(asStringRaw(item["error"])) != ""
case "dynamicToolCall":
if success, ok := item["success"].(bool); ok && !success {
failed = true
}
}
if failed {
return "failed", true
}
return status, false
}