package service import ( "sync" edgeevents "iop/apps/edge/internal/events" edgenode "iop/apps/edge/internal/node" "iop/packages/go/config" ) const ( DefaultSessionID = "default" DefaultTimeoutSec = 30 ) // Service is the surface-neutral application service shared by the console, // HTTP/A2A input surfaces, and the Control Plane connector. Its responsibilities // are split across this package by concern: status_provider.go (node snapshots, // capabilities, health status), run_dispatch.go (run lifecycle), node_command.go // (node command transport), and control_command.go (Control Plane command // execution). type Service struct { mu sync.RWMutex registry *edgenode.Registry events *edgeevents.Bus nodeStore *edgenode.NodeStore queue *modelQueueManager modelCatalog []config.ModelCatalogEntry tunnels *providerTunnelRouter } func New(registry *edgenode.Registry, events *edgeevents.Bus) *Service { s := &Service{registry: registry, events: events, tunnels: newProviderTunnelRouter()} if events != nil { q := newModelQueueManager(nil) q.startEventWatcher(events) s.queue = q } return s } func (s *Service) SetNodeStore(store *edgenode.NodeStore) { s.mu.Lock() s.nodeStore = store s.mu.Unlock() if s.queue != nil { s.queue.setStore(store) } } // SetRuntimeConfig atomically replaces the runtime config snapshot used by // dispatch and status readers. A reader observes either the old node // store/catalog pair or the new pair, never a mix. func (s *Service) SetRuntimeConfig(store *edgenode.NodeStore, catalog []config.ModelCatalogEntry) { catalog = cloneModelCatalog(catalog) s.mu.Lock() s.nodeStore = store s.modelCatalog = catalog s.mu.Unlock() if s.queue != nil { s.queue.setStore(store) } } // SetModelCatalog provides the top-level provider-pool model catalog to the // service. Must be called before the first provider-pool SubmitRun. func (s *Service) SetModelCatalog(catalog []config.ModelCatalogEntry) { s.mu.Lock() s.modelCatalog = cloneModelCatalog(catalog) s.mu.Unlock() } func (s *Service) runtimeConfigSnapshot() (*edgenode.NodeStore, []config.ModelCatalogEntry) { s.mu.RLock() defer s.mu.RUnlock() return s.nodeStore, cloneModelCatalog(s.modelCatalog) } func cloneModelCatalog(catalog []config.ModelCatalogEntry) []config.ModelCatalogEntry { if len(catalog) == 0 { return nil } out := make([]config.ModelCatalogEntry, len(catalog)) for i, entry := range catalog { out[i] = entry if entry.Providers != nil { out[i].Providers = make(map[string]string, len(entry.Providers)) for k, v := range entry.Providers { out[i].Providers[k] = v } } } return out } func (s *Service) ListNodes() []*edgenode.NodeEntry { return s.registry.All() } // NormalizeSessionID maps an empty session id to the default session. func NormalizeSessionID(id string) string { if id == "" { return DefaultSessionID } return id } func normalizeTimeoutSec(timeoutSec int) int { if timeoutSec <= 0 { return DefaultTimeoutSec } return timeoutSec } func nodeLabel(entry *edgenode.NodeEntry) string { return entry.DisplayLabel() }