package scheduler import ( "context" "log" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/store" ) // TaskConfig holds the runtime task configuration read from global_settings. // Keys: tasks.enabled, tasks.allow_personal, tasks.max_concurrent, // tasks.default_max_tokens, tasks.default_max_tool_calls, // tasks.default_max_wall_clock type TaskConfig struct { Enabled bool AllowPersonal bool MaxConcurrent int DefaultMaxTokens int DefaultMaxToolCalls int DefaultMaxWallClock int // seconds } // DefaultTaskConfig returns sensible defaults when no global config is set. func DefaultTaskConfig() TaskConfig { return TaskConfig{ Enabled: true, AllowPersonal: true, MaxConcurrent: 5, DefaultMaxTokens: 4096, DefaultMaxToolCalls: 10, DefaultMaxWallClock: 300, } } // LoadTaskConfig reads task configuration from global_settings. // Falls back to defaults for missing keys. func LoadTaskConfig(ctx context.Context, gc store.GlobalConfigStore) TaskConfig { cfg := DefaultTaskConfig() if gc == nil { return cfg } raw, err := gc.Get(ctx, "tasks") if err != nil || raw == nil { return cfg } if v, ok := boolVal(raw, "enabled"); ok { cfg.Enabled = v } if v, ok := boolVal(raw, "allow_personal"); ok { cfg.AllowPersonal = v } if v, ok := intVal(raw, "max_concurrent"); ok && v > 0 { cfg.MaxConcurrent = v } if v, ok := intVal(raw, "default_max_tokens"); ok && v > 0 { cfg.DefaultMaxTokens = v } if v, ok := intVal(raw, "default_max_tool_calls"); ok && v > 0 { cfg.DefaultMaxToolCalls = v } if v, ok := intVal(raw, "default_max_wall_clock"); ok && v > 0 { cfg.DefaultMaxWallClock = v } return cfg } // ApplyDefaults fills zero-value budget fields on a task with the global defaults. func (tc TaskConfig) ApplyDefaults(t *models.Task) { if t.MaxTokens == 0 { t.MaxTokens = tc.DefaultMaxTokens } if t.MaxToolCalls == 0 { t.MaxToolCalls = tc.DefaultMaxToolCalls } if t.MaxWallClock == 0 { t.MaxWallClock = tc.DefaultMaxWallClock } } // ── helpers ──────────────────────────────────── func boolVal(m models.JSONMap, key string) (bool, bool) { v, ok := m[key] if !ok { return false, false } b, ok := v.(bool) return b, ok } func intVal(m models.JSONMap, key string) (int, bool) { v, ok := m[key] if !ok { return 0, false } // JSON numbers are float64 after Unmarshal switch n := v.(type) { case float64: return int(n), true case int: return n, true default: log.Printf("[task_config] unexpected type for %s: %T", key, v) return 0, false } }