mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 12:05:06 +02:00
* feat(agent): add Pi agent runtime support
Add Pi as a new agent runtime provider, following the established adapter
pattern. Pi CLI outputs JSONL events which are parsed for messages, tool
calls, and usage tracking.
Backend:
- New piBackend implementing the Backend interface (pi.go)
- Pi CLI discovery via MULTICA_PI_PATH env var or PATH lookup
- JSONL event stream parsing (agent_start, message_update, thinking_update,
tool_execution_start/end, agent_end)
- Usage scanner for ~/.pi/sessions/*.jsonl files
- Runtime config injection via AGENTS.md
- Skill injection to .pi/agent/skills/
Frontend:
- Pi provider logo (teal π icon)
- Pi label in transcript dialog
Docs:
- Updated all provider lists in README, CLI_INSTALL, and docs
* fix(agent): filter Pi usage scanner to agent_end events only
Address review feedback: restrict usage parsing to agent_end events
which contain cumulative totals, preventing potential inaccuracy if
Pi adds usage fields to other event types in the future.
* fix(agent): align Pi runtime with real CLI flags, event schema, and custom_args
- Flags: Pi's CLI uses `--mode json` (not `--output-format jsonl`), has no
`--yolo` (explicit `--tools` allowlist instead), takes the prompt as a
positional argument (not `-p <prompt>`), splits model as
`--provider <name> --model <id>`, and treats `--session` as a file path
that must exist before spawn.
- Event parsing: rewrite the stream event struct to match Pi's actual
JSON event schema (`message_update.assistantMessageEvent.delta`,
`turn_end.message.usage.{input,output,cacheRead,cacheWrite}`, etc.).
- Sessions: generate/persist session files under ~/.multica/pi-sessions/
and use the file path as the opaque SessionID returned to the daemon.
- Usage scanner: read assistant `message` events from the same session
files (Pi's session-file schema, distinct from the stdout stream).
- Custom args: consume `ExecOptions.CustomArgs` via `filterCustomArgs`
with a Pi-specific blocked set (`-p`, `--print`, `--mode`, `--session`)
so Pi matches the pattern shared by every other agent backend.
83 lines
2.2 KiB
Go
83 lines
2.2 KiB
Go
package usage
|
|
|
|
import (
|
|
"log/slog"
|
|
)
|
|
|
|
// Record represents aggregated token usage for one (date, provider, model) tuple.
|
|
type Record struct {
|
|
Date string `json:"date"` // "2006-01-02"
|
|
Provider string `json:"provider"` // "claude" or "codex"
|
|
Model string `json:"model"`
|
|
InputTokens int64 `json:"input_tokens"`
|
|
OutputTokens int64 `json:"output_tokens"`
|
|
CacheReadTokens int64 `json:"cache_read_tokens"`
|
|
CacheWriteTokens int64 `json:"cache_write_tokens"`
|
|
}
|
|
|
|
// Scanner scans local CLI log files for token usage data.
|
|
type Scanner struct {
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewScanner creates a new usage scanner.
|
|
func NewScanner(logger *slog.Logger) *Scanner {
|
|
return &Scanner{logger: logger}
|
|
}
|
|
|
|
// Scan reads local log files for all supported agent runtimes (Claude Code,
|
|
// Codex, OpenCode, OpenClaw, Hermes) and returns aggregated usage records
|
|
// keyed by (date, provider, model). Supports Claude Code, Codex, OpenCode,
|
|
// OpenClaw, Hermes, and Pi.
|
|
func (s *Scanner) Scan() []Record {
|
|
var records []Record
|
|
|
|
claudeRecords := s.scanClaude()
|
|
records = append(records, claudeRecords...)
|
|
|
|
codexRecords := s.scanCodex()
|
|
records = append(records, codexRecords...)
|
|
|
|
openCodeRecords := s.scanOpenCode()
|
|
records = append(records, openCodeRecords...)
|
|
|
|
openClawRecords := s.scanOpenClaw()
|
|
records = append(records, openClawRecords...)
|
|
|
|
hermesRecords := s.scanHermes()
|
|
records = append(records, hermesRecords...)
|
|
|
|
piRecords := s.scanPi()
|
|
records = append(records, piRecords...)
|
|
|
|
return records
|
|
}
|
|
|
|
// aggregation key for merging records.
|
|
type aggKey struct {
|
|
Date string
|
|
Provider string
|
|
Model string
|
|
}
|
|
|
|
func mergeRecords(records []Record) []Record {
|
|
m := make(map[aggKey]*Record)
|
|
for _, r := range records {
|
|
k := aggKey{Date: r.Date, Provider: r.Provider, Model: r.Model}
|
|
if existing, ok := m[k]; ok {
|
|
existing.InputTokens += r.InputTokens
|
|
existing.OutputTokens += r.OutputTokens
|
|
existing.CacheReadTokens += r.CacheReadTokens
|
|
existing.CacheWriteTokens += r.CacheWriteTokens
|
|
} else {
|
|
copy := r
|
|
m[k] = ©
|
|
}
|
|
}
|
|
result := make([]Record, 0, len(m))
|
|
for _, r := range m {
|
|
result = append(result, *r)
|
|
}
|
|
return result
|
|
}
|