mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
* feat(runtimes): remove Test Connection / runtime ping feature The Test Connection action invoked a real single-turn agent run to verify runtime connectivity. In practice it was expensive (reuses none of the normal task exec env, so it also gave misleading results) and low value — daemon heartbeat + Online status already covers the "is the runtime alive" question. Dropping the whole end-to-end probe path: - deletes server handler and in-memory PingStore - drops pending_ping from the heartbeat response and daemon poll loop - removes daemon.handlePing, PendingPing, ReportPingResult - removes the CLI `multica runtime ping` command - removes the PingSection UI block and RuntimePing types / api methods * docs: fix runtime CLI subcommand list in product-overview
241 lines
6.3 KiB
Go
241 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/multica-ai/multica/server/internal/cli"
|
|
)
|
|
|
|
var runtimeCmd = &cobra.Command{
|
|
Use: "runtime",
|
|
Short: "Work with agent runtimes",
|
|
}
|
|
|
|
var runtimeListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List runtimes in the workspace",
|
|
RunE: runRuntimeList,
|
|
}
|
|
|
|
var runtimeUsageCmd = &cobra.Command{
|
|
Use: "usage <runtime-id>",
|
|
Short: "Get token usage for a runtime",
|
|
Args: exactArgs(1),
|
|
RunE: runRuntimeUsage,
|
|
}
|
|
|
|
var runtimeActivityCmd = &cobra.Command{
|
|
Use: "activity <runtime-id>",
|
|
Short: "Get hourly task activity for a runtime",
|
|
Args: exactArgs(1),
|
|
RunE: runRuntimeActivity,
|
|
}
|
|
|
|
var runtimeUpdateCmd = &cobra.Command{
|
|
Use: "update <runtime-id>",
|
|
Short: "Initiate a CLI update on a runtime",
|
|
Args: exactArgs(1),
|
|
RunE: runRuntimeUpdate,
|
|
}
|
|
|
|
func init() {
|
|
runtimeCmd.AddCommand(runtimeListCmd)
|
|
runtimeCmd.AddCommand(runtimeUsageCmd)
|
|
runtimeCmd.AddCommand(runtimeActivityCmd)
|
|
runtimeCmd.AddCommand(runtimeUpdateCmd)
|
|
|
|
// runtime list
|
|
runtimeListCmd.Flags().String("output", "table", "Output format: table or json")
|
|
|
|
// runtime usage
|
|
runtimeUsageCmd.Flags().String("output", "table", "Output format: table or json")
|
|
runtimeUsageCmd.Flags().Int("days", 90, "Number of days of usage data to retrieve (max 365)")
|
|
|
|
// runtime activity
|
|
runtimeActivityCmd.Flags().String("output", "table", "Output format: table or json")
|
|
|
|
// runtime update
|
|
runtimeUpdateCmd.Flags().String("target-version", "", "Target version to update to (required)")
|
|
runtimeUpdateCmd.Flags().String("output", "json", "Output format: table or json")
|
|
runtimeUpdateCmd.Flags().Bool("wait", false, "Wait for update to complete (poll until done)")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Runtime commands
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func runRuntimeList(cmd *cobra.Command, _ []string) error {
|
|
client, err := newAPIClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
var runtimes []map[string]any
|
|
if err := client.GetJSON(ctx, "/api/runtimes", &runtimes); err != nil {
|
|
return fmt.Errorf("list runtimes: %w", err)
|
|
}
|
|
|
|
output, _ := cmd.Flags().GetString("output")
|
|
if output == "json" {
|
|
return cli.PrintJSON(os.Stdout, runtimes)
|
|
}
|
|
|
|
headers := []string{"ID", "NAME", "MODE", "PROVIDER", "STATUS", "LAST_SEEN"}
|
|
rows := make([][]string, 0, len(runtimes))
|
|
for _, rt := range runtimes {
|
|
rows = append(rows, []string{
|
|
strVal(rt, "id"),
|
|
strVal(rt, "name"),
|
|
strVal(rt, "runtime_mode"),
|
|
strVal(rt, "provider"),
|
|
strVal(rt, "status"),
|
|
strVal(rt, "last_seen_at"),
|
|
})
|
|
}
|
|
cli.PrintTable(os.Stdout, headers, rows)
|
|
return nil
|
|
}
|
|
|
|
func runRuntimeUsage(cmd *cobra.Command, args []string) error {
|
|
client, err := newAPIClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
days, _ := cmd.Flags().GetInt("days")
|
|
if days < 1 || days > 365 {
|
|
return fmt.Errorf("--days must be between 1 and 365")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
var usage []map[string]any
|
|
path := fmt.Sprintf("/api/runtimes/%s/usage?days=%d", args[0], days)
|
|
if err := client.GetJSON(ctx, path, &usage); err != nil {
|
|
return fmt.Errorf("get runtime usage: %w", err)
|
|
}
|
|
|
|
output, _ := cmd.Flags().GetString("output")
|
|
if output == "json" {
|
|
return cli.PrintJSON(os.Stdout, usage)
|
|
}
|
|
|
|
headers := []string{"DATE", "PROVIDER", "MODEL", "INPUT_TOKENS", "OUTPUT_TOKENS", "CACHE_READ", "CACHE_WRITE"}
|
|
rows := make([][]string, 0, len(usage))
|
|
for _, u := range usage {
|
|
rows = append(rows, []string{
|
|
strVal(u, "date"),
|
|
strVal(u, "provider"),
|
|
strVal(u, "model"),
|
|
strVal(u, "input_tokens"),
|
|
strVal(u, "output_tokens"),
|
|
strVal(u, "cache_read_tokens"),
|
|
strVal(u, "cache_write_tokens"),
|
|
})
|
|
}
|
|
cli.PrintTable(os.Stdout, headers, rows)
|
|
return nil
|
|
}
|
|
|
|
func runRuntimeActivity(cmd *cobra.Command, args []string) error {
|
|
client, err := newAPIClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
var activity []map[string]any
|
|
if err := client.GetJSON(ctx, "/api/runtimes/"+args[0]+"/activity", &activity); err != nil {
|
|
return fmt.Errorf("get runtime activity: %w", err)
|
|
}
|
|
|
|
output, _ := cmd.Flags().GetString("output")
|
|
if output == "json" {
|
|
return cli.PrintJSON(os.Stdout, activity)
|
|
}
|
|
|
|
headers := []string{"HOUR", "COUNT"}
|
|
rows := make([][]string, 0, len(activity))
|
|
for _, a := range activity {
|
|
rows = append(rows, []string{
|
|
strVal(a, "hour"),
|
|
strVal(a, "count"),
|
|
})
|
|
}
|
|
cli.PrintTable(os.Stdout, headers, rows)
|
|
return nil
|
|
}
|
|
|
|
func runRuntimeUpdate(cmd *cobra.Command, args []string) error {
|
|
client, err := newAPIClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
targetVersion, _ := cmd.Flags().GetString("target-version")
|
|
if targetVersion == "" {
|
|
return fmt.Errorf("--target-version is required")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Second)
|
|
defer cancel()
|
|
|
|
body := map[string]any{
|
|
"target_version": targetVersion,
|
|
}
|
|
|
|
var update map[string]any
|
|
if err := client.PostJSON(ctx, "/api/runtimes/"+args[0]+"/update", body, &update); err != nil {
|
|
return fmt.Errorf("initiate update: %w", err)
|
|
}
|
|
|
|
wait, _ := cmd.Flags().GetBool("wait")
|
|
if !wait {
|
|
output, _ := cmd.Flags().GetString("output")
|
|
if output == "json" {
|
|
return cli.PrintJSON(os.Stdout, update)
|
|
}
|
|
fmt.Printf("Update initiated: %s (status: %s)\n", strVal(update, "id"), strVal(update, "status"))
|
|
return nil
|
|
}
|
|
|
|
// Poll until completed/failed/timeout.
|
|
updateID := strVal(update, "id")
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("timed out waiting for update (last status: %s)", strVal(update, "status"))
|
|
case <-time.After(2 * time.Second):
|
|
}
|
|
|
|
if err := client.GetJSON(ctx, "/api/runtimes/"+args[0]+"/update/"+updateID, &update); err != nil {
|
|
return fmt.Errorf("get update status: %w", err)
|
|
}
|
|
|
|
status := strVal(update, "status")
|
|
if status == "completed" || status == "failed" || status == "timeout" {
|
|
output, _ := cmd.Flags().GetString("output")
|
|
if output == "json" {
|
|
return cli.PrintJSON(os.Stdout, update)
|
|
}
|
|
if status == "completed" {
|
|
fmt.Printf("Update completed: %s\n", strVal(update, "output"))
|
|
} else {
|
|
fmt.Printf("Update %s: %s\n", status, strVal(update, "error"))
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|