a76ec74338
C++ ImGui kanban for steering LLM agents. Six panels (Board, Calendar, Dashboard, Agent runs, Worktrees, DoD inspector) wired to registry functions http_request, kpi_card, sparkline, agent_runs_timeline, dod_evidence_panel. Backend Go on :8403 (independent operations.db from kanban_web).
87 lines
2.1 KiB
Go
87 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ChatLogger appends one JSON line per tool invocation to a file. Thread-safe.
|
|
// Format per line: {"ts":"...","tool":"...","input":{...},"ok":bool,"error":"...","result_summary":"..."}
|
|
type ChatLogger struct {
|
|
path string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func newChatLogger(path string) *ChatLogger {
|
|
return &ChatLogger{path: path}
|
|
}
|
|
|
|
type ChatLogEntry struct {
|
|
TS string `json:"ts"`
|
|
Tool string `json:"tool"`
|
|
Input json.RawMessage `json:"input"`
|
|
OK bool `json:"ok"`
|
|
Error string `json:"error,omitempty"`
|
|
ResultSummary string `json:"result_summary,omitempty"`
|
|
}
|
|
|
|
func (l *ChatLogger) Log(tool string, input json.RawMessage, res ToolResult) {
|
|
if l == nil || l.path == "" {
|
|
return
|
|
}
|
|
entry := ChatLogEntry{
|
|
TS: time.Now().UTC().Format(time.RFC3339Nano),
|
|
Tool: tool,
|
|
Input: input,
|
|
OK: res.OK,
|
|
Error: res.Error,
|
|
}
|
|
if res.OK && res.Result != nil {
|
|
entry.ResultSummary = summarizeResult(res.Result)
|
|
}
|
|
line, err := json.Marshal(entry)
|
|
if err != nil {
|
|
return
|
|
}
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
f, err := os.OpenFile(l.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
f.Write(line)
|
|
f.Write([]byte("\n"))
|
|
}
|
|
|
|
// summarizeResult produces a short description of a tool result for the log.
|
|
// Keeps the log line compact: full payloads can be reconstructed from operations.db.
|
|
func summarizeResult(v any) string {
|
|
switch r := v.(type) {
|
|
case *Column:
|
|
return fmt.Sprintf("column %s name=%q", r.ID, r.Name)
|
|
case *Card:
|
|
return fmt.Sprintf("card %s title=%q col=%s", r.ID, r.Title, r.ColumnID)
|
|
case []Card:
|
|
return fmt.Sprintf("%d cards", len(r))
|
|
case []HistoryEntry:
|
|
return fmt.Sprintf("%d history entries", len(r))
|
|
case map[string]any:
|
|
// list_board shape
|
|
cols, _ := r["columns"].([]Column)
|
|
cards, _ := r["cards"].([]Card)
|
|
return fmt.Sprintf("board: %d cols, %d cards", len(cols), len(cards))
|
|
}
|
|
b, err := json.Marshal(v)
|
|
if err != nil || len(b) == 0 {
|
|
return ""
|
|
}
|
|
if len(b) > 200 {
|
|
return string(b[:200]) + "..."
|
|
}
|
|
return string(b)
|
|
}
|