Files
zzgandClaude Opus 5 79fe343be5 Move the module path to git.z.gd/zzg/fathom
The module was declared as github.com/zzg/fathom, which is not where
this repo lives and never has been. Nothing is published at that path,
so `go get github.com/zzg/fathom` resolves to a 404 while the import
paths in every file assert otherwise.

git.z.gd is a real module path rather than a cosmetic correction:
Gitea serves the go-import meta tag for it,

  <meta name="go-import" content="git.z.gd/zzg/fathom git https://git.z.gd/zzg/fathom.git">

and the repo answers unauthenticated over HTTPS, so `go get
git.z.gd/zzg/fathom` resolves where the old path could not.

Mechanical: the module line plus 46 import paths across 23 files. There
is no go.sum to churn, since fathom has no third-party dependencies.
go build, go vet, gofmt and the full test suite are clean under the new
path, and the built binary still runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:31:18 -07:00

88 lines
2.8 KiB
Go

package ingest
import (
"strings"
"git.z.gd/zzg/fathom/internal/model"
)
// classify maps a span onto one of Fathom's kinds.
//
// The kind drives colour and grouping in the UI, so a wrong answer is cosmetic
// rather than lossy — the original span name is always kept verbatim. That is
// why this is a heuristic and not a registry: the alternative is demanding that
// every emitter be taught to Fathom before its traces render usefully, which
// would defeat the point of speaking plain OTLP.
//
// Attributes are checked before names because they are the more deliberate
// signal. Claude Code sets span.type; OpenTelemetry's GenAI conventions set
// gen_ai.operation.name; OpenLLMetry sets llm.request.type.
func classify(name string, attrs model.Attrs) model.Kind {
if k, ok := classifyByAttrs(attrs); ok {
return k
}
return classifyByName(name)
}
func classifyByAttrs(attrs model.Attrs) (model.Kind, bool) {
if v, ok := attrs["span.type"]; ok {
switch v.Str {
case "llm_request":
return model.KindLLMCall, true
case "tool", "tool.execution", "tool.blocked_on_user":
return model.KindToolCall, true
case "interaction", "subagent", "compaction":
return model.KindAgentStep, true
case "mcp.rpc":
return model.KindMCP, true
case "hook":
return model.KindHook, true
}
}
if v, ok := attrs["gen_ai.operation.name"]; ok {
switch v.Str {
case "chat", "text_completion", "generate_content":
return model.KindLLMCall, true
case "execute_tool":
return model.KindToolCall, true
case "embeddings":
return model.KindRetrieval, true
}
}
// A span carrying a tool call id is a tool call, whatever it is named.
if _, ok := attrs["gen_ai.tool.call.id"]; ok {
return model.KindToolCall, true
}
if _, ok := attrs["tool_name"]; ok {
return model.KindToolCall, true
}
// Likewise, a span reporting a model and token counts is a model call.
if _, ok := attrs["gen_ai.request.model"]; ok {
return model.KindLLMCall, true
}
return model.KindOther, false
}
func classifyByName(name string) model.Kind {
n := strings.ToLower(name)
switch {
case strings.Contains(n, "mcp"):
return model.KindMCP
case strings.Contains(n, "hook"):
return model.KindHook
case strings.Contains(n, "tool"):
return model.KindToolCall
case strings.Contains(n, "llm"), strings.Contains(n, "chat"),
strings.Contains(n, "completion"), strings.Contains(n, "gen_ai"):
return model.KindLLMCall
case strings.Contains(n, "retriev"), strings.Contains(n, "embed"),
strings.Contains(n, "vector"), strings.Contains(n, "search"):
return model.KindRetrieval
case strings.Contains(n, "agent"), strings.Contains(n, "interaction"),
strings.Contains(n, "chain"), strings.Contains(n, "workflow"),
strings.Contains(n, "task"):
return model.KindAgentStep
}
return model.KindOther
}