Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions cmd/aima/context.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"time"

"github.com/jguan/aima/internal/agent"
"github.com/jguan/aima/internal/k3s"
"github.com/jguan/aima/internal/knowledge"
Expand All @@ -15,17 +17,18 @@ import (
// It collects the local variables that buildToolDeps() closures previously
// captured, enabling those closures to be split into separate files.
type appContext struct {
cat *knowledge.Catalog
db *state.DB
kStore *knowledge.Store
rt runtime.Runtime // default runtime (K3S > Docker > Native)
nativeRt runtime.Runtime
dockerRt runtime.Runtime
k3sRt runtime.Runtime
proxy *proxy.Server
k3s *k3s.Client
dataDir string
digests map[string]string // factory catalog digests
support *support.Service
eventBus *agent.EventBus // shared EventBus for Explorer events
cat *knowledge.Catalog
db *state.DB
kStore *knowledge.Store
rt runtime.Runtime // default runtime (K3S > Docker > Native)
nativeRt runtime.Runtime
dockerRt runtime.Runtime
k3sRt runtime.Runtime
proxy *proxy.Server
k3s *k3s.Client
dataDir string
catalogLoadedAt time.Time
digests map[string]string // factory catalog digests
support *support.Service
eventBus *agent.EventBus // shared EventBus for Explorer events
}
3 changes: 2 additions & 1 deletion cmd/aima/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ func buildDiagnosticsBundle(ctx context.Context, ac *appContext, deps *mcp.ToolD
"goarch": goruntime.GOARCH,
"data_dir": redactHomePath(dataDir),
},
"sections": map[string]any{},
"service_context": serviceContextStatus(ac),
"sections": map[string]any{},
}

sections := bundle["sections"].(map[string]any)
Expand Down
27 changes: 27 additions & 0 deletions cmd/aima/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,30 @@ func TestBuildDiagnosticsBundleRecordsSectionErrors(t *testing.T) {
t.Fatalf("hardware error = %v, want context canceled", hardware["error"])
}
}

func TestServiceContextReportsStaleOverlayHint(t *testing.T) {
tmp := t.TempDir()
overlayDir := tmp + string(os.PathSeparator) + "catalog" + string(os.PathSeparator) + "user" + string(os.PathSeparator) + "models"
if err := os.MkdirAll(overlayDir, 0o755); err != nil {
t.Fatalf("mkdir overlay: %v", err)
}
overlayFile := overlayDir + string(os.PathSeparator) + "demo.patch.yaml"
if err := os.WriteFile(overlayFile, []byte("kind: model_asset_patch\nmetadata:\n name: demo\n"), 0o644); err != nil {
t.Fatalf("write overlay: %v", err)
}
newer := time.Now().Add(5 * time.Minute)
if err := os.Chtimes(overlayFile, newer, newer); err != nil {
t.Fatalf("chtimes overlay: %v", err)
}

status := serviceContextStatus(&appContext{
dataDir: tmp,
catalogLoadedAt: time.Now(),
})
if status["overlay_newer_than_catalog"] != true {
t.Fatalf("overlay_newer_than_catalog = %v, want true; status=%v", status["overlay_newer_than_catalog"], status)
}
if status["reload_hint"] == "" {
t.Fatalf("reload_hint missing: %v", status)
}
}
25 changes: 13 additions & 12 deletions cmd/aima/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,19 @@ func run() error {
mcpServer := mcp.NewServer()
supportSvc := support.NewService(db, support.WithLogger(slog.Default()))
ac := &appContext{
cat: cat,
db: db,
kStore: knowledgeStore,
rt: rt,
nativeRt: nativeRt,
dockerRt: dockerRt,
k3sRt: k3sRt,
proxy: proxyServer,
k3s: k3sClient,
dataDir: dataDir,
digests: factoryDigests,
support: supportSvc,
cat: cat,
db: db,
kStore: knowledgeStore,
rt: rt,
nativeRt: nativeRt,
dockerRt: dockerRt,
k3sRt: k3sRt,
proxy: proxyServer,
k3s: k3sClient,
dataDir: dataDir,
catalogLoadedAt: time.Now().UTC(),
digests: factoryDigests,
support: supportSvc,
}
deps := buildToolDeps(ac)

Expand Down
40 changes: 34 additions & 6 deletions cmd/aima/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ var autoDetectWarned sync.Map
// resolvedDeployment holds the shared result of resolve + CheckFit,
// used by both DeployApply and DeployDryRun.
type resolvedDeployment struct {
ModelName string
Resolved *knowledge.ResolvedConfig
Fit *knowledge.FitReport
ModelName string
Resolved *knowledge.ResolvedConfig
ResolvedConfig map[string]any
ResolvedProvenance map[string]string
Fit *knowledge.FitReport
}

// queryGoldenOverrides returns config overrides from the best golden configuration
Expand Down Expand Up @@ -117,19 +119,45 @@ func resolveDeployment(ctx context.Context, cat *knowledge.Catalog, db *state.DB
return nil, err
}

resolvedConfig := cloneAnyMap(resolved.Config)
resolvedProvenance := cloneStringMap(resolved.Provenance)
fit := knowledge.CheckFit(resolved, hwInfo)
for k, v := range fit.Adjustments {
resolved.Config[k] = v
resolved.Provenance[k] = "L0-auto"
}

return &resolvedDeployment{
ModelName: canonicalName,
Resolved: resolved,
Fit: fit,
ModelName: canonicalName,
Resolved: resolved,
ResolvedConfig: resolvedConfig,
ResolvedProvenance: resolvedProvenance,
Fit: fit,
}, nil
}

func cloneAnyMap(in map[string]any) map[string]any {
if in == nil {
return nil
}
out := make(map[string]any, len(in))
for k, v := range in {
out[k] = v
}
return out
}

func cloneStringMap(in map[string]string) map[string]string {
if in == nil {
return nil
}
out := make(map[string]string, len(in))
for k, v := range in {
out[k] = v
}
return out
}

// normalizeAutoPortOverrides removes "auto" sentinels from port-like override keys
// before resolution. This preserves the engine YAML default port so Go-side host
// port allocation can still choose a free host port later in deploy.apply.
Expand Down
64 changes: 64 additions & 0 deletions cmd/aima/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,70 @@ func TestNormalizeAutoPortOverrides(t *testing.T) {
}
}

func TestResolveDeploymentKeepsResolvedAndEffectiveConfigSeparate(t *testing.T) {
ctx := context.Background()
db, err := state.Open(ctx, ":memory:")
if err != nil {
t.Fatalf("Open: %v", err)
}
defer db.Close()

cat := &knowledge.Catalog{
EngineAssets: []knowledge.EngineAsset{{
Metadata: knowledge.EngineMetadata{
Name: "vllm-test",
Type: "vllm",
Version: "1.0",
SupportedFormats: []string{"safetensors"},
},
Hardware: knowledge.EngineHardware{GPUArch: "*"},
Startup: knowledge.EngineStartup{
Command: []string{"vllm", "serve", "{{.ModelPath}}"},
DefaultArgs: map[string]any{"gpu_memory_utilization": 0.85, "port": 8000},
},
Runtime: knowledge.EngineRuntime{Default: "container"},
}},
ModelAssets: []knowledge.ModelAsset{{
Metadata: knowledge.ModelMetadata{Name: "demo-model", Type: "llm"},
Storage: knowledge.ModelStorage{DefaultPathPattern: "/models/demo"},
Variants: []knowledge.ModelVariant{{
Name: "demo-model-vllm",
Engine: "vllm",
Format: "safetensors",
Hardware: knowledge.ModelVariantHardware{GPUArch: "Blackwell"},
}},
}},
}

rd, err := resolveDeployment(ctx, cat, db, nil, knowledge.HardwareInfo{
GPUArch: "Blackwell",
GPUVRAMMiB: 122880,
GPUMemFreeMiB: 64000,
GPUMemUsedMiB: 58880,
UnifiedMemory: false,
Platform: "linux/arm64",
}, "demo-model", "vllm", "", nil, t.TempDir())
if err != nil {
t.Fatalf("resolveDeployment: %v", err)
}

if got := rd.ResolvedConfig["gpu_memory_utilization"]; got != 0.85 {
t.Fatalf("resolved_config gpu_memory_utilization = %v, want 0.85", got)
}
if got := rd.Resolved.Config["gpu_memory_utilization"]; got != 0.51 {
t.Fatalf("effective config gpu_memory_utilization = %v, want 0.51", got)
}
if got := rd.Fit.Adjustments["gpu_memory_utilization"]; got != 0.51 {
t.Fatalf("fit adjustment gpu_memory_utilization = %v, want 0.51", got)
}
if got := rd.ResolvedProvenance["gpu_memory_utilization"]; got != "L0" {
t.Fatalf("resolved provenance = %q, want L0", got)
}
if got := rd.Resolved.Provenance["gpu_memory_utilization"]; got != "L0-auto" {
t.Fatalf("effective provenance = %q, want L0-auto", got)
}
}

func TestResolveCatalogWithLocalEngineOverlayUsesInstalledContainerAsset(t *testing.T) {
ctx := context.Background()
db, err := state.Open(ctx, ":memory:")
Expand Down
65 changes: 65 additions & 0 deletions cmd/aima/service_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"os"
"path/filepath"
"strings"
"time"
)

func serviceContextStatus(ac *appContext) map[string]any {
dataDir := ""
var loadedAt time.Time
if ac != nil {
dataDir = strings.TrimSpace(ac.dataDir)
loadedAt = ac.catalogLoadedAt
}
overlayDir := ""
if dataDir != "" {
overlayDir = filepath.Join(dataDir, "catalog")
}
latestOverlay := latestModTime(overlayDir)

status := map[string]any{
"data_dir": dataDir,
"overlay_dir": overlayDir,
}
if !loadedAt.IsZero() {
status["catalog_loaded_at"] = loadedAt.Format(time.RFC3339)
}
if !latestOverlay.IsZero() {
status["overlay_latest_mtime"] = latestOverlay.Format(time.RFC3339)
if !loadedAt.IsZero() && latestOverlay.After(loadedAt) {
status["overlay_newer_than_catalog"] = true
status["reload_hint"] = "catalog overlays changed after this AIMA process loaded; restart aima-serve or reload catalog before trusting UI dry-run results"
}
}
if user := os.Getenv("USER"); user != "" {
status["user"] = user
}
if home, err := os.UserHomeDir(); err == nil && home != "" {
status["home_dir"] = home
}
return status
}

func latestModTime(root string) time.Time {
if strings.TrimSpace(root) == "" {
return time.Time{}
}
var latest time.Time
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil || d == nil {
return nil
}
info, statErr := d.Info()
if statErr != nil {
return nil
}
if info.ModTime().After(latest) {
latest = info.ModTime()
}
return nil
})
return latest
}
22 changes: 14 additions & 8 deletions cmd/aima/tooldeps_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
PortSpecs: append([]knowledge.StartupPort(nil), resolved.PortSpecs...),
InitCommands: resolved.InitCommands,
ModelPath: modelPath,
ModelType: catalogModelType(cat, modelName),
Config: resolved.Config,
RuntimeClassName: resolved.RuntimeClassName,
CPUArch: resolved.CPUArch,
Expand Down Expand Up @@ -346,14 +347,19 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps,
}

result := map[string]any{
"model": rd.ModelName,
"engine": resolved.Engine,
"engine_image": resolved.EngineImage,
"slot": resolved.Slot,
"runtime": runtimeName,
"config": resolved.Config,
"ports": knowledge.ResolvePortBindingsFromSpecs(resolved.PortSpecs, resolved.Config),
"provenance": resolved.Provenance,
"model": rd.ModelName,
"engine": resolved.Engine,
"engine_image": resolved.EngineImage,
"slot": resolved.Slot,
"runtime": runtimeName,
"config": resolved.Config,
"resolved_config": rd.ResolvedConfig,
"effective_config": resolved.Config,
"fit_adjustments": rd.Fit.Adjustments,
"ports": knowledge.ResolvePortBindingsFromSpecs(resolved.PortSpecs, resolved.Config),
"provenance": resolved.Provenance,
"resolved_provenance": rd.ResolvedProvenance,
"effective_provenance": resolved.Provenance,
"fit_report": map[string]any{
"fit": rd.Fit.Fit,
"reason": rd.Fit.Reason,
Expand Down
9 changes: 5 additions & 4 deletions cmd/aima/tooldeps_knowledge.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,10 +591,11 @@ func buildKnowledgeDeps(ac *appContext, deps *mcp.ToolDeps) {
}
}
status := map[string]any{
"factory_assets": catalogSize(factoryCat),
"overlay_assets": catalogSize(overlayCat),
"shadowed": shadowed,
"parse_warnings": parseWarnings,
"factory_assets": catalogSize(factoryCat),
"overlay_assets": catalogSize(overlayCat),
"shadowed": shadowed,
"parse_warnings": parseWarnings,
"service_context": serviceContextStatus(ac),
}
return json.Marshal(status)
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/aima/tooldeps_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ func buildSystemDeps(ac *appContext, deps *mcp.ToolDeps) {
if b, e := json.Marshal(ac.support.Status(ctx)); e == nil {
status["support"] = b
}
if b, e := json.Marshal(serviceContextStatus(ac)); e == nil {
status["service_context"] = b
}
if deps.OpenClawStatus != nil {
if b, e := deps.OpenClawStatus(ctx); e == nil {
status["openclaw"] = b
Expand Down
Loading
Loading