chore: import upstream snapshot with attribution
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,958 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const ZCodeDBName = "db.sqlite"
|
||||
|
||||
const zcodeDBName = ZCodeDBName
|
||||
|
||||
type zcodeProviderFactory struct {
|
||||
def AgentDef
|
||||
}
|
||||
|
||||
func newZcodeProviderFactory(def AgentDef) ProviderFactory {
|
||||
return zcodeProviderFactory{def: cloneAgentDef(def)}
|
||||
}
|
||||
|
||||
func (f zcodeProviderFactory) Definition() AgentDef {
|
||||
return cloneAgentDef(f.def)
|
||||
}
|
||||
|
||||
func (f zcodeProviderFactory) Capabilities() Capabilities {
|
||||
return zcodeProviderCapabilities()
|
||||
}
|
||||
|
||||
func (f zcodeProviderFactory) NewProvider(cfg ProviderConfig) Provider {
|
||||
cfg = cfg.Clone()
|
||||
cfg.Roots = normalizeZCodeRoots(cfg.Roots)
|
||||
spec := zcodeProviderSpec()
|
||||
return &dbBackedProvider{
|
||||
ProviderBase: ProviderBase{
|
||||
Def: cloneAgentDef(f.def),
|
||||
Caps: spec.caps,
|
||||
Config: cfg,
|
||||
},
|
||||
spec: spec,
|
||||
sources: newDBBackedSourceSet(spec, cfg.Roots),
|
||||
}
|
||||
}
|
||||
|
||||
func zcodeProviderCapabilities() Capabilities {
|
||||
return Capabilities{
|
||||
Source: dbBackedSourceCapabilities(CapabilityNotApplicable),
|
||||
Content: ContentCapabilities{
|
||||
FirstMessage: CapabilitySupported,
|
||||
SessionName: CapabilitySupported,
|
||||
Cwd: CapabilitySupported,
|
||||
Thinking: CapabilitySupported,
|
||||
ToolCalls: CapabilitySupported,
|
||||
ToolResults: CapabilitySupported,
|
||||
AggregateUsageEvents: CapabilitySupported,
|
||||
Model: CapabilitySupported,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func zcodeProviderSpec() dbBackedProviderSpec {
|
||||
return dbBackedProviderSpec{
|
||||
agent: AgentZCode,
|
||||
dbName: zcodeDBName,
|
||||
findDB: zcodeDBPath,
|
||||
listMeta: func(dbPath string) ([]dbBackedSessionMeta, error) {
|
||||
return listZCodeSessionMeta(dbPath)
|
||||
},
|
||||
parse: func(dbPath, sessionID, machine string) ([]ParseResult, error) {
|
||||
result, err := parseZCodeSession(dbPath, sessionID, machine)
|
||||
if err != nil || result == nil {
|
||||
return nil, err
|
||||
}
|
||||
return []ParseResult{*result}, nil
|
||||
},
|
||||
caps: zcodeProviderCapabilities(),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeZCodeRoots(roots []string) []string {
|
||||
cleaned := cleanJSONLRoots(roots)
|
||||
out := make([]string, 0, len(cleaned))
|
||||
seen := make(map[string]struct{}, len(cleaned))
|
||||
for _, root := range cleaned {
|
||||
normalized := normalizeZCodeRoot(root)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[normalized]; ok {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
out = append(out, normalized)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeZCodeRoot(root string) string {
|
||||
root = filepath.Clean(root)
|
||||
if root == "" || root == "." {
|
||||
return ""
|
||||
}
|
||||
if filepath.Base(root) == "db" {
|
||||
return root
|
||||
}
|
||||
return filepath.Join(root, "db")
|
||||
}
|
||||
|
||||
func zcodeDBPath(dir string) string {
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
path := filepath.Join(dir, zcodeDBName)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func zcodeVirtualPathParts(path string) (string, string, bool) {
|
||||
return ParseVirtualSourcePathForBase(path, zcodeDBName)
|
||||
}
|
||||
|
||||
func ZCodeSQLiteVirtualPath(dbPath, sessionID string) string {
|
||||
return VirtualSourcePath(dbPath, sessionID)
|
||||
}
|
||||
|
||||
func ZCodeSQLiteSourceMtime(path string) (int64, error) {
|
||||
dbPath, sessionID, ok := zcodeVirtualPathParts(path)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("not a zcode sqlite virtual path: %s", path)
|
||||
}
|
||||
db, err := openZCodeDB(dbPath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
row, err := loadZCodeSessionRow(db, sessionID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return zcodeSessionFileMtime(dbPath, db, row), nil
|
||||
}
|
||||
|
||||
func listZCodeSessionMeta(dbPath string) ([]dbBackedSessionMeta, error) {
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
db, err := openZCodeDB(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
COALESCE(directory, ''),
|
||||
COALESCE(title, ''),
|
||||
COALESCE(time_created, ''),
|
||||
COALESCE(time_updated, '')
|
||||
FROM session
|
||||
ORDER BY COALESCE(time_updated, time_created), id
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing zcode sessions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var metas []dbBackedSessionMeta
|
||||
for rows.Next() {
|
||||
row, err := scanZCodeSessionRow(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.id == "" {
|
||||
continue
|
||||
}
|
||||
metas = append(metas, dbBackedSessionMeta{
|
||||
SessionID: row.id,
|
||||
VirtualPath: ZCodeSQLiteVirtualPath(dbPath, row.id),
|
||||
FileMtime: zcodeSessionFileMtime(dbPath, db, row),
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(metas, func(i, j int) bool {
|
||||
return metas[i].SessionID < metas[j].SessionID
|
||||
})
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
func parseZCodeSession(dbPath, sessionID, machine string) (*ParseResult, error) {
|
||||
db, err := openZCodeDB(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
row, err := loadZCodeSessionRow(db, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := buildZCodeParseResult(dbPath, machine, row, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func openZCodeDB(dbPath string) (*sql.DB, error) {
|
||||
dsn := "file:" + sqliteURIPath(dbPath) + "?mode=ro&immutable=0&_busy_timeout=3000"
|
||||
db, err := sql.Open("sqlite3", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening zcode db %s: %w", dbPath, err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
type zcodeSessionRow struct {
|
||||
id string
|
||||
projectID sql.NullString
|
||||
workspaceID sql.NullString
|
||||
directory sql.NullString
|
||||
title sql.NullString
|
||||
timeCreated sql.NullString
|
||||
timeUpdated sql.NullString
|
||||
}
|
||||
|
||||
type zcodeUsageRow struct {
|
||||
sessionID string
|
||||
turnID sql.NullString
|
||||
providerID sql.NullString
|
||||
modelID sql.NullString
|
||||
status sql.NullString
|
||||
inputTokens sql.NullInt64
|
||||
outputTokens sql.NullInt64
|
||||
reasoningTokens sql.NullInt64
|
||||
cacheCreationInputTokens sql.NullInt64
|
||||
cacheReadInputTokens sql.NullInt64
|
||||
computedTotalTokens sql.NullInt64
|
||||
startedAt sql.NullString
|
||||
completedAt sql.NullString
|
||||
durationMS sql.NullInt64
|
||||
toolCallCount sql.NullInt64
|
||||
}
|
||||
|
||||
type zcodeMessageRow struct {
|
||||
id string
|
||||
data string
|
||||
timeCreated string
|
||||
}
|
||||
|
||||
type zcodePartRow struct {
|
||||
id string
|
||||
messageID string
|
||||
timeCreated string
|
||||
data string
|
||||
}
|
||||
|
||||
func loadZCodeSessionRow(db *sql.DB, sessionID string) (zcodeSessionRow, error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
COALESCE(directory, ''),
|
||||
COALESCE(title, ''),
|
||||
COALESCE(time_created, ''),
|
||||
COALESCE(time_updated, '')
|
||||
FROM session
|
||||
WHERE id = ?
|
||||
`, sessionID)
|
||||
|
||||
var out zcodeSessionRow
|
||||
err := row.Scan(
|
||||
&out.id, &out.projectID, &out.workspaceID,
|
||||
&out.directory, &out.title, &out.timeCreated, &out.timeUpdated,
|
||||
)
|
||||
if err != nil {
|
||||
return zcodeSessionRow{}, fmt.Errorf("loading zcode session %s: %w", sessionID, err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func scanZCodeSessionRow(rows *sql.Rows) (zcodeSessionRow, error) {
|
||||
var out zcodeSessionRow
|
||||
if err := rows.Scan(
|
||||
&out.id, &out.projectID, &out.workspaceID,
|
||||
&out.directory, &out.title, &out.timeCreated, &out.timeUpdated,
|
||||
); err != nil {
|
||||
return zcodeSessionRow{}, fmt.Errorf("scanning zcode session meta: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildZCodeParseResult(
|
||||
dbPath, machine string,
|
||||
row zcodeSessionRow,
|
||||
db *sql.DB,
|
||||
) (ParseResult, error) {
|
||||
startedAt := zcodeParseTime(row.timeCreated.String)
|
||||
endedAt := zcodeParseTime(row.timeUpdated.String)
|
||||
if startedAt.IsZero() {
|
||||
startedAt = endedAt
|
||||
}
|
||||
if endedAt.IsZero() {
|
||||
endedAt = startedAt
|
||||
}
|
||||
|
||||
directory := row.directory.String
|
||||
project := ExtractProjectFromCwd(directory)
|
||||
if project == "" {
|
||||
switch {
|
||||
case row.projectID.Valid:
|
||||
project = "project-" + strings.TrimSpace(row.projectID.String)
|
||||
case row.workspaceID.Valid:
|
||||
project = "workspace-" + strings.TrimSpace(row.workspaceID.String)
|
||||
default:
|
||||
project = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(row.title.String)
|
||||
firstMessage := title
|
||||
if firstMessage != "" {
|
||||
firstMessage = truncate(strings.ReplaceAll(firstMessage, "\n", " "), 300)
|
||||
}
|
||||
|
||||
msgs, err := loadZCodeMessages(db, row.id)
|
||||
if err != nil {
|
||||
return ParseResult{}, err
|
||||
}
|
||||
parts, err := loadZCodeParts(db, row.id)
|
||||
if err != nil {
|
||||
return ParseResult{}, err
|
||||
}
|
||||
parsedMessages := buildZCodeMessages(msgs, parts)
|
||||
userMessageCount := 0
|
||||
for _, msg := range parsedMessages {
|
||||
if msg.Role == RoleUser {
|
||||
userMessageCount++
|
||||
}
|
||||
}
|
||||
|
||||
sess := ParsedSession{
|
||||
ID: "zcode:" + row.id,
|
||||
Project: project,
|
||||
Machine: machine,
|
||||
Agent: AgentZCode,
|
||||
Cwd: directory,
|
||||
FirstMessage: firstMessage,
|
||||
SessionName: title,
|
||||
StartedAt: startedAt,
|
||||
EndedAt: endedAt,
|
||||
MessageCount: len(parsedMessages),
|
||||
UserMessageCount: userMessageCount,
|
||||
File: FileInfo{
|
||||
Path: ZCodeSQLiteVirtualPath(dbPath, row.id),
|
||||
},
|
||||
}
|
||||
if info, err := os.Stat(dbPath); err == nil {
|
||||
sess.File.Size = info.Size()
|
||||
}
|
||||
sess.File.Mtime = zcodeSessionFileMtime(dbPath, db, row)
|
||||
|
||||
usageEvents, err := listZCodeUsageEvents(db, row.id, startedAt, endedAt)
|
||||
if err != nil {
|
||||
return ParseResult{}, err
|
||||
}
|
||||
applyUsageEventTokenTotals(&sess, usageEvents)
|
||||
|
||||
return ParseResult{
|
||||
Session: sess,
|
||||
Messages: parsedMessages,
|
||||
UsageEvents: usageEvents,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadZCodeMessages(
|
||||
db *sql.DB, sessionID string,
|
||||
) ([]zcodeMessageRow, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id,
|
||||
COALESCE(data, '{}'),
|
||||
CAST(COALESCE(time_created, '') AS TEXT)
|
||||
FROM message
|
||||
WHERE session_id = ?
|
||||
ORDER BY CAST(COALESCE(time_created, '') AS TEXT), id
|
||||
`, sessionID)
|
||||
if err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "no such table: message") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("listing zcode messages for %s: %w", sessionID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
msgs := make([]zcodeMessageRow, 0)
|
||||
for rows.Next() {
|
||||
var row zcodeMessageRow
|
||||
if err := rows.Scan(&row.id, &row.data, &row.timeCreated); err != nil {
|
||||
return nil, fmt.Errorf("scanning zcode message row: %w", err)
|
||||
}
|
||||
if row.id == "" {
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
func loadZCodeParts(
|
||||
db *sql.DB, sessionID string,
|
||||
) (map[string][]zcodePartRow, error) {
|
||||
selectTime := `''`
|
||||
orderBy := `id`
|
||||
if has, err := zcodeTableHasColumn(db, "part", "time_created"); err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
selectTime = `CAST(COALESCE(time_created, '') AS TEXT)`
|
||||
orderBy = selectTime + `, id`
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id,
|
||||
message_id,
|
||||
`+selectTime+`,
|
||||
COALESCE(data, '{}')
|
||||
FROM part
|
||||
WHERE session_id = ?
|
||||
ORDER BY `+orderBy, sessionID)
|
||||
if err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "no such table: part") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("listing zcode parts for %s: %w", sessionID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
parts := make(map[string][]zcodePartRow)
|
||||
for rows.Next() {
|
||||
var row zcodePartRow
|
||||
if err := rows.Scan(&row.id, &row.messageID, &row.timeCreated, &row.data); err != nil {
|
||||
return nil, fmt.Errorf("scanning zcode part row: %w", err)
|
||||
}
|
||||
if row.messageID == "" {
|
||||
continue
|
||||
}
|
||||
parts[row.messageID] = append(parts[row.messageID], row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
func buildZCodeMessages(
|
||||
msgs []zcodeMessageRow,
|
||||
parts map[string][]zcodePartRow,
|
||||
) []ParsedMessage {
|
||||
if msgs == nil || parts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsed := make([]ParsedMessage, 0, len(msgs))
|
||||
for _, row := range msgs {
|
||||
msg, ok := buildZCodeMessage(len(parsed), row, parts[row.id])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
parsed = append(parsed, msg)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func buildZCodeMessage(
|
||||
ordinal int,
|
||||
row zcodeMessageRow,
|
||||
parts []zcodePartRow,
|
||||
) (ParsedMessage, bool) {
|
||||
role, ok := normalizeZCodeRole(gjson.Get(row.data, "role").Str)
|
||||
if !ok {
|
||||
return ParsedMessage{}, false
|
||||
}
|
||||
|
||||
msg := ParsedMessage{
|
||||
Ordinal: ordinal,
|
||||
Role: role,
|
||||
Timestamp: zcodeParseTime(row.timeCreated),
|
||||
Model: zcodeMessageModel(row.data),
|
||||
IsSystem: role == RoleSystem,
|
||||
}
|
||||
|
||||
var texts []string
|
||||
var thinking []string
|
||||
for _, part := range parts {
|
||||
block := gjson.Parse(part.data)
|
||||
switch block.Get("type").Str {
|
||||
case "text":
|
||||
if text := zcodeBlockText(block); text != "" {
|
||||
texts = append(texts, text)
|
||||
}
|
||||
case "thinking", "reasoning":
|
||||
text := zcodeThinkingText(block)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
msg.HasThinking = true
|
||||
thinking = append(thinking, text)
|
||||
texts = append(texts, "[Thinking]\n"+text+"\n[/Thinking]")
|
||||
case "tool_use", "tool":
|
||||
msg.HasToolUse = true
|
||||
if tc, ok := zcodeParseToolCall(block); ok {
|
||||
msg.ToolCalls = append(msg.ToolCalls, tc)
|
||||
}
|
||||
if tr, ok := zcodeParseToolResult(block); ok {
|
||||
msg.ToolResults = append(msg.ToolResults, tr)
|
||||
}
|
||||
case "tool_result":
|
||||
if tr, ok := zcodeParseToolResult(block); ok {
|
||||
msg.ToolResults = append(msg.ToolResults, tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg.Content = strings.Join(texts, "\n")
|
||||
msg.ThinkingText = strings.Join(thinking, "\n\n")
|
||||
msg.ContentLength = len(msg.Content)
|
||||
return msg, true
|
||||
}
|
||||
|
||||
func normalizeZCodeRole(role string) (RoleType, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case "user":
|
||||
return RoleUser, true
|
||||
case "assistant", "model":
|
||||
return RoleAssistant, true
|
||||
case "system":
|
||||
return RoleSystem, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func zcodeMessageModel(raw string) string {
|
||||
for _, path := range []string{
|
||||
"modelID",
|
||||
"model_id",
|
||||
"model.modelID",
|
||||
"model.id",
|
||||
"model.name",
|
||||
"model",
|
||||
} {
|
||||
if model := strings.TrimSpace(gjson.Get(raw, path).Str); model != "" {
|
||||
return model
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func zcodeBlockText(block gjson.Result) string {
|
||||
for _, path := range []string{"text", "content", "value"} {
|
||||
value := block.Get(path)
|
||||
if !value.Exists() {
|
||||
continue
|
||||
}
|
||||
if text := decodeContent(value); text != "" {
|
||||
return text
|
||||
}
|
||||
if value.Type == gjson.String && value.Str != "" {
|
||||
return value.Str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func zcodeThinkingText(block gjson.Result) string {
|
||||
for _, path := range []string{"thinking", "text", "content"} {
|
||||
value := block.Get(path)
|
||||
if !value.Exists() {
|
||||
continue
|
||||
}
|
||||
if text := decodeContent(value); text != "" {
|
||||
return text
|
||||
}
|
||||
if value.Type == gjson.String && value.Str != "" {
|
||||
return value.Str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func zcodeParseToolCall(block gjson.Result) (ParsedToolCall, bool) {
|
||||
name := block.Get("name").Str
|
||||
if name == "" {
|
||||
name = block.Get("tool_name").Str
|
||||
}
|
||||
if name == "" {
|
||||
name = block.Get("toolName").Str
|
||||
}
|
||||
if name == "" {
|
||||
name = block.Get("tool").Str
|
||||
}
|
||||
if name == "" {
|
||||
return ParsedToolCall{}, false
|
||||
}
|
||||
|
||||
input := toolCallInput(block)
|
||||
if input.Raw == "" || input.Raw == "{}" {
|
||||
if stateInput := block.Get("state.input"); stateInput.Exists() {
|
||||
input = stateInput
|
||||
}
|
||||
}
|
||||
inputJSON := input.Raw
|
||||
if inputJSON == "" {
|
||||
inputJSON = "{}"
|
||||
}
|
||||
toolUseID := block.Get("id").Str
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("tool_use_id").Str
|
||||
}
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("toolUseID").Str
|
||||
}
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("callID").Str
|
||||
}
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("callId").Str
|
||||
}
|
||||
|
||||
call := ParsedToolCall{
|
||||
ToolUseID: toolUseID,
|
||||
ToolName: name,
|
||||
Category: NormalizeToolCategory(name),
|
||||
InputJSON: inputJSON,
|
||||
}
|
||||
switch name {
|
||||
case "Skill", "skill":
|
||||
call.SkillName = input.Get("skill").Str
|
||||
if call.SkillName == "" {
|
||||
call.SkillName = input.Get("name").Str
|
||||
}
|
||||
default:
|
||||
call.SkillName = inferToolSkillName(name, inputJSON)
|
||||
}
|
||||
return call, true
|
||||
}
|
||||
|
||||
func zcodeParseToolResult(block gjson.Result) (ParsedToolResult, bool) {
|
||||
toolUseID := block.Get("tool_use_id").Str
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("toolUseID").Str
|
||||
}
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("tool_call_id").Str
|
||||
}
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("callID").Str
|
||||
}
|
||||
if toolUseID == "" {
|
||||
toolUseID = block.Get("callId").Str
|
||||
}
|
||||
if toolUseID == "" {
|
||||
return ParsedToolResult{}, false
|
||||
}
|
||||
|
||||
content := block.Get("content")
|
||||
if !content.Exists() || content.Type == gjson.Null {
|
||||
content = block.Get("result")
|
||||
}
|
||||
if !content.Exists() || content.Type == gjson.Null {
|
||||
content = block.Get("output")
|
||||
}
|
||||
if !content.Exists() || content.Type == gjson.Null {
|
||||
content = block.Get("state.output")
|
||||
}
|
||||
if !content.Exists() || content.Type == gjson.Null {
|
||||
content = block.Get("state.error")
|
||||
}
|
||||
if content.Exists() && content.Type != gjson.Null {
|
||||
return ParsedToolResult{
|
||||
ToolUseID: toolUseID,
|
||||
ContentLength: toolResultContentLength(content),
|
||||
ContentRaw: content.Raw,
|
||||
}, true
|
||||
}
|
||||
|
||||
for _, key := range []string{"text", "error", "value"} {
|
||||
if text := block.Get(key).Str; text != "" {
|
||||
return ParsedToolResult{
|
||||
ToolUseID: toolUseID,
|
||||
ContentLength: len(text),
|
||||
ContentRaw: strconv.Quote(text),
|
||||
}, true
|
||||
}
|
||||
}
|
||||
return ParsedToolResult{}, false
|
||||
}
|
||||
|
||||
func zcodeTableHasColumn(db *sql.DB, table, column string) (bool, error) {
|
||||
rows, err := db.Query(`PRAGMA table_info(` + table + `)`)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("listing zcode table info for %s: %w", table, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
cid int
|
||||
name string
|
||||
typeName string
|
||||
notNull int
|
||||
defaultV sql.NullString
|
||||
primaryKey int
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&cid, &name, &typeName, ¬Null, &defaultV, &primaryKey,
|
||||
); err != nil {
|
||||
return false, fmt.Errorf("scanning zcode table info for %s: %w", table, err)
|
||||
}
|
||||
if strings.EqualFold(name, column) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func listZCodeUsageEvents(
|
||||
db *sql.DB,
|
||||
sessionID string,
|
||||
startedAt, endedAt time.Time,
|
||||
) ([]ParsedUsageEvent, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT session_id,
|
||||
CAST(turn_id AS TEXT),
|
||||
provider_id,
|
||||
COALESCE(model_id, ''),
|
||||
COALESCE(status, ''),
|
||||
COALESCE(input_tokens, 0),
|
||||
COALESCE(output_tokens, 0),
|
||||
COALESCE(reasoning_tokens, 0),
|
||||
COALESCE(cache_creation_input_tokens, 0),
|
||||
COALESCE(cache_read_input_tokens, 0),
|
||||
COALESCE(computed_total_tokens, 0),
|
||||
COALESCE(started_at, ''),
|
||||
COALESCE(completed_at, ''),
|
||||
COALESCE(duration_ms, 0),
|
||||
COALESCE(tool_call_count, 0)
|
||||
FROM model_usage
|
||||
WHERE session_id = ?
|
||||
ORDER BY
|
||||
COALESCE(turn_id, -1),
|
||||
COALESCE(started_at, ''),
|
||||
COALESCE(completed_at, ''),
|
||||
COALESCE(provider_id, -1),
|
||||
COALESCE(model_id, ''),
|
||||
COALESCE(status, ''),
|
||||
COALESCE(input_tokens, 0),
|
||||
COALESCE(output_tokens, 0),
|
||||
COALESCE(reasoning_tokens, 0),
|
||||
COALESCE(cache_creation_input_tokens, 0),
|
||||
COALESCE(cache_read_input_tokens, 0),
|
||||
COALESCE(computed_total_tokens, 0),
|
||||
COALESCE(duration_ms, 0),
|
||||
COALESCE(tool_call_count, 0)
|
||||
`, sessionID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such table: model_usage") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("listing zcode usage rows for %s: %w", sessionID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var events []ParsedUsageEvent
|
||||
for rows.Next() {
|
||||
row, err := scanZCodeUsageRow(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.sessionID == "" {
|
||||
continue
|
||||
}
|
||||
fullSessionID := "zcode:" + row.sessionID
|
||||
ev := ParsedUsageEvent{
|
||||
SessionID: fullSessionID,
|
||||
Source: "session",
|
||||
Model: row.modelID.String,
|
||||
InputTokens: int(row.inputTokens.Int64),
|
||||
OutputTokens: int(row.outputTokens.Int64),
|
||||
CacheCreationInputTokens: int(row.cacheCreationInputTokens.Int64),
|
||||
CacheReadInputTokens: int(row.cacheReadInputTokens.Int64),
|
||||
ReasoningTokens: int(row.reasoningTokens.Int64),
|
||||
OccurredAt: timeString(zcodeUsageTime(row.completedAt.String), zcodeUsageTime(row.startedAt.String)),
|
||||
DedupKey: zcodeUsageDedupKey(fullSessionID, row),
|
||||
}
|
||||
if row.turnID.Valid {
|
||||
if parsed, err := strconv.Atoi(strings.TrimSpace(row.turnID.String)); err == nil {
|
||||
ev.MessageOrdinal = &parsed
|
||||
}
|
||||
}
|
||||
if ev.OccurredAt == "" {
|
||||
ev.OccurredAt = timeString(endedAt, startedAt)
|
||||
}
|
||||
events = append(events, ev)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func scanZCodeUsageRow(rows *sql.Rows) (zcodeUsageRow, error) {
|
||||
var out zcodeUsageRow
|
||||
if err := rows.Scan(
|
||||
&out.sessionID, &out.turnID, &out.providerID,
|
||||
&out.modelID, &out.status, &out.inputTokens, &out.outputTokens,
|
||||
&out.reasoningTokens, &out.cacheCreationInputTokens,
|
||||
&out.cacheReadInputTokens, &out.computedTotalTokens,
|
||||
&out.startedAt, &out.completedAt, &out.durationMS,
|
||||
&out.toolCallCount,
|
||||
); err != nil {
|
||||
return zcodeUsageRow{}, fmt.Errorf("scanning zcode usage row: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func zcodeUsageDedupKey(fullSessionID string, row zcodeUsageRow) string {
|
||||
parts := []string{
|
||||
"session:" + fullSessionID,
|
||||
"turn=" + row.turnID.String,
|
||||
"provider=" + row.providerID.String,
|
||||
"model=" + row.modelID.String,
|
||||
"status=" + row.status.String,
|
||||
"started_at=" + row.startedAt.String,
|
||||
"completed_at=" + row.completedAt.String,
|
||||
"duration_ms=" + zcodeNullInt64String(row.durationMS),
|
||||
"tool_call_count=" + zcodeNullInt64String(row.toolCallCount),
|
||||
"input_tokens=" + zcodeNullInt64String(row.inputTokens),
|
||||
"output_tokens=" + zcodeNullInt64String(row.outputTokens),
|
||||
"reasoning_tokens=" + zcodeNullInt64String(row.reasoningTokens),
|
||||
"cache_creation_input_tokens=" + zcodeNullInt64String(row.cacheCreationInputTokens),
|
||||
"cache_read_input_tokens=" + zcodeNullInt64String(row.cacheReadInputTokens),
|
||||
"computed_total_tokens=" + zcodeNullInt64String(row.computedTotalTokens),
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
func zcodeNullInt64String(v sql.NullInt64) string {
|
||||
if !v.Valid {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(v.Int64, 10)
|
||||
}
|
||||
|
||||
func zcodeParseTime(raw string) time.Time {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
if ts := parseTimestamp(raw); !ts.IsZero() {
|
||||
return ts
|
||||
}
|
||||
if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||
switch {
|
||||
case n >= 1_000_000_000_000_000_000:
|
||||
return time.Unix(0, n).UTC()
|
||||
case n >= 1_000_000_000_000:
|
||||
return time.UnixMilli(n).UTC()
|
||||
case n >= 1_000_000_000:
|
||||
return time.Unix(n, 0).UTC()
|
||||
default:
|
||||
return time.Unix(n, 0).UTC()
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func zcodeUsageTime(raw string) time.Time {
|
||||
return zcodeParseTime(raw)
|
||||
}
|
||||
|
||||
func zcodeSessionFileMtime(dbPath string, db *sql.DB, row zcodeSessionRow) int64 {
|
||||
maxMtime := zcodeTimeUnixNano(zcodeRowUpdatedAt(row))
|
||||
if usageMtime, err := zcodeMaxUsageMtime(db, row.id); err == nil {
|
||||
maxMtime = max(maxMtime, usageMtime)
|
||||
}
|
||||
for _, path := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} {
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
maxMtime = max(maxMtime, info.ModTime().UnixNano())
|
||||
}
|
||||
}
|
||||
return maxMtime
|
||||
}
|
||||
|
||||
func zcodeMaxUsageMtime(db *sql.DB, sessionID string) (int64, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT CAST(COALESCE(completed_at, '') AS TEXT),
|
||||
CAST(COALESCE(started_at, '') AS TEXT)
|
||||
FROM model_usage
|
||||
WHERE session_id = ?
|
||||
`, sessionID)
|
||||
if err != nil {
|
||||
if strings.Contains(strings.ToLower(err.Error()), "no such table: model_usage") {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("listing zcode usage mtimes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var maxMtime int64
|
||||
for rows.Next() {
|
||||
var completedAt, startedAt string
|
||||
if err := rows.Scan(&completedAt, &startedAt); err != nil {
|
||||
return 0, fmt.Errorf("scanning zcode usage mtime: %w", err)
|
||||
}
|
||||
maxMtime = max(maxMtime, zcodeTimeUnixNano(zcodeUsageTime(completedAt)))
|
||||
maxMtime = max(maxMtime, zcodeTimeUnixNano(zcodeUsageTime(startedAt)))
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return maxMtime, nil
|
||||
}
|
||||
|
||||
func zcodeTimeUnixNano(ts time.Time) int64 {
|
||||
if ts.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return ts.UnixNano()
|
||||
}
|
||||
|
||||
func zcodeRowUpdatedAt(row zcodeSessionRow) time.Time {
|
||||
if ts := zcodeParseTime(row.timeUpdated.String); !ts.IsZero() {
|
||||
return ts
|
||||
}
|
||||
if ts := zcodeParseTime(row.timeCreated.String); !ts.IsZero() {
|
||||
return ts
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
Reference in New Issue
Block a user