diff options
| author | Adam Malczewski <[email protected]> | 2026-05-21 19:06:15 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-21 19:06:15 +0900 |
| commit | 55633c90c0d96e62153a4b6655f3f833a3b46ad4 (patch) | |
| tree | a75f97b48071eb1ba9e8366d8053a376dde69af5 /packages/core/src | |
| parent | d6b208342edf97bafa5b1dcc986b782f9879d141 (diff) | |
| download | dispatch-55633c90c0d96e62153a4b6655f3f833a3b46ad4.tar.gz dispatch-55633c90c0d96e62153a4b6655f3f833a3b46ad4.zip | |
refactor: gut model/tag/fallback/agent-template system, fix Docker setup
- Remove ModelResolver, model definitions, model tags, fallback order, agent templates
- Remove all [[models]], [agents], and fallback from dispatch.toml and config schema
- ModelRegistry is now a pure key-state manager
- dispatch.toml reduced to keys + permissions only
- Docker: fix entrypoint for existing UID, skip bun install in frontend container
- Docker: scoped build cache prune in bin/clean
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/config/schema.ts | 126 | ||||
| -rw-r--r-- | packages/core/src/index.ts | 2 | ||||
| -rw-r--r-- | packages/core/src/models/index.ts | 1 | ||||
| -rw-r--r-- | packages/core/src/models/registry.ts | 64 | ||||
| -rw-r--r-- | packages/core/src/models/resolver.ts | 88 | ||||
| -rw-r--r-- | packages/core/src/types/index.ts | 25 |
6 files changed, 11 insertions, 295 deletions
diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index 83e2695..2cd8d55 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -1,9 +1,7 @@ import type { - AgentTemplate, ConfigError, DispatchConfig, KeyDefinition, - ModelDefinition, } from "../types/index.js"; function isRecord(value: unknown): value is Record<string, unknown> { @@ -58,69 +56,6 @@ function validatePermissions( return result; } -function validateAgentTemplate( - raw: unknown, - path: string, - errors: ConfigError[], -): AgentTemplate | null { - if (!isRecord(raw)) { - errors.push({ path, message: "must be an object" }); - return null; - } - - const requiredStrings = ["name", "description", "system_prompt", "model_tag"] as const; - for (const field of requiredStrings) { - if (typeof raw[field] !== "string") { - errors.push({ path: `${path}.${field}`, message: "must be a string" }); - return null; - } - } - - if (!Array.isArray(raw["tools"]) || !raw["tools"].every((t) => typeof t === "string")) { - errors.push({ path: `${path}.tools`, message: "must be an array of strings" }); - return null; - } - - const perms = validatePermissions(raw["permissions"] ?? {}, `${path}.permissions`, errors); - - return { - name: raw["name"] as string, - description: raw["description"] as string, - system_prompt: raw["system_prompt"] as string, - tools: raw["tools"] as string[], - model_tag: raw["model_tag"] as string, - permissions: perms, - }; -} - -function validateModel(raw: unknown, path: string, errors: ConfigError[]): ModelDefinition | null { - if (!isRecord(raw)) { - errors.push({ path, message: "must be an object" }); - return null; - } - if (typeof raw["id"] !== "string") { - errors.push({ path: `${path}.id`, message: "must be a string" }); - return null; - } - if (typeof raw["provider"] !== "string") { - errors.push({ path: `${path}.provider`, message: "must be a string" }); - return null; - } - if ( - !Array.isArray(raw["tags"]) || - raw["tags"].length === 0 || - !raw["tags"].every((t) => typeof t === "string") - ) { - errors.push({ path: `${path}.tags`, message: "must be a non-empty array of strings" }); - return null; - } - return { - id: raw["id"] as string, - provider: raw["provider"] as string, - tags: raw["tags"] as string[], - }; -} - function validateKey(raw: unknown, path: string, errors: ConfigError[]): KeyDefinition | null { if (!isRecord(raw)) { errors.push({ path, message: "must be an object" }); @@ -169,34 +104,6 @@ export function validateConfig(raw: unknown): { config: DispatchConfig; errors: // permissions (required, but can be empty) const permissions = validatePermissions(raw["permissions"] ?? {}, "permissions", errors); - // agents (optional) - let agents: Record<string, AgentTemplate> | undefined; - if (raw["agents"] !== undefined) { - if (!isRecord(raw["agents"])) { - errors.push({ path: "agents", message: "must be an object" }); - } else { - agents = {}; - for (const [key, value] of Object.entries(raw["agents"])) { - const agent = validateAgentTemplate(value, `agents.${key}`, errors); - if (agent) agents[key] = agent; - } - } - } - - // models (optional) - let models: ModelDefinition[] | undefined; - if (raw["models"] !== undefined) { - if (!Array.isArray(raw["models"])) { - errors.push({ path: "models", message: "must be an array" }); - } else { - models = []; - for (let i = 0; i < raw["models"].length; i++) { - const model = validateModel(raw["models"][i], `models[${i}]`, errors); - if (model) models.push(model); - } - } - } - // keys (optional) let keys: KeyDefinition[] | undefined; if (raw["keys"] !== undefined) { @@ -211,42 +118,9 @@ export function validateConfig(raw: unknown): { config: DispatchConfig; errors: } } - // fallback (optional) - let fallback: string[] | undefined; - if (raw["fallback"] !== undefined) { - if (!Array.isArray(raw["fallback"]) || !raw["fallback"].every((f) => typeof f === "string")) { - errors.push({ path: "fallback", message: "must be an array of strings" }); - } else { - fallback = raw["fallback"] as string[]; - // Validate that referenced key IDs exist - const keyIds = new Set((keys ?? []).map((k) => k.id)); - for (const id of fallback) { - if (!keyIds.has(id)) { - errors.push({ path: "fallback", message: `key id "${id}" not found in keys` }); - } - } - } - } - - // Warn if agent model_tags don't match any model - if (agents && models) { - const allTags = new Set(models.flatMap((m) => m.tags)); - for (const [name, agent] of Object.entries(agents)) { - if (!allTags.has(agent.model_tag)) { - errors.push({ - path: `agents.${name}.model_tag`, - message: `no model found with tag "${agent.model_tag}"`, - }); - } - } - } - const config: DispatchConfig = { permissions, - ...(agents !== undefined && { agents }), - ...(models !== undefined && { models }), ...(keys !== undefined && { keys }), - ...(fallback !== undefined && { fallback }), }; return { config, errors }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 90cafbe..b1e2e14 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -25,7 +25,7 @@ export { loadConfig, configToRuleset, validateConfig, createConfigWatcher } from export { parseSkillFile, loadSkills, resolveSkillsForAgent, getSkillByName, createSkillsWatcher } from "./skills/index.js"; // Models -export { ModelRegistry, ModelResolver } from "./models/index.js"; +export { ModelRegistry } from "./models/index.js"; // Credentials export * from "./credentials/index.js"; diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index e5efcc8..cf59749 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -1,2 +1 @@ export { ModelRegistry } from "./registry.js"; -export { ModelResolver } from "./resolver.js"; diff --git a/packages/core/src/models/registry.ts b/packages/core/src/models/registry.ts index f78fe0e..9ea0b32 100644 --- a/packages/core/src/models/registry.ts +++ b/packages/core/src/models/registry.ts @@ -1,25 +1,20 @@ -import type { KeyDefinition, KeyState, ModelDefinition } from "../types/index.js"; +import type { KeyDefinition, KeyState } from "../types/index.js"; export class ModelRegistry { - private models: ModelDefinition[]; private keyStates: Map<string, KeyState>; - private fallbackOrder: string[]; + private keyOrder: string[]; - constructor(models: ModelDefinition[], keys: KeyDefinition[], fallbackOrder: string[]) { - this.models = []; + constructor(keys: KeyDefinition[]) { this.keyStates = new Map(); - this.fallbackOrder = []; - this._initConfig(models, keys, fallbackOrder, new Map()); + this.keyOrder = []; + this._initConfig(keys, new Map()); } private _initConfig( - models: ModelDefinition[], keys: KeyDefinition[], - fallbackOrder: string[], existingStates: Map<string, KeyState>, ): void { - this.models = [...models]; - this.fallbackOrder = this._buildFallbackOrder(keys, fallbackOrder); + this.keyOrder = keys.map((k) => k.id); const newStates = new Map<string, KeyState>(); for (const key of keys) { @@ -34,51 +29,12 @@ export class ModelRegistry { this.keyStates = newStates; } - private _buildFallbackOrder(keys: KeyDefinition[], fallbackOrder: string[]): string[] { - const ordered: string[] = []; - const keyIds = new Set(keys.map((k) => k.id)); - - // Add keys from fallbackOrder first (if they exist) - for (const id of fallbackOrder) { - if (keyIds.has(id) && !ordered.includes(id)) { - ordered.push(id); - } - } - - // Append remaining keys not in fallbackOrder - for (const key of keys) { - if (!ordered.includes(key.id)) { - ordered.push(key.id); - } - } - - return ordered; - } - - getModels(): ModelDefinition[] { - return [...this.models]; - } - getKeys(): KeyState[] { - return this.fallbackOrder + return this.keyOrder .map((id) => this.keyStates.get(id)) .filter((state): state is KeyState => state !== undefined); } - getModelsByTag(tag: string): ModelDefinition[] { - return this.models.filter((m) => m.tags.includes(tag)); - } - - getAllTags(): string[] { - const tags = new Set<string>(); - for (const model of this.models) { - for (const tag of model.tags) { - tags.add(tag); - } - } - return [...tags]; - } - markKeyExhausted(keyId: string, error?: string): void { const state = this.keyStates.get(keyId); if (!state) return; @@ -118,13 +74,13 @@ export class ModelRegistry { return true; } - updateConfig(models: ModelDefinition[], keys: KeyDefinition[], fallbackOrder: string[]): void { - this._initConfig(models, keys, fallbackOrder, this.keyStates); + updateConfig(keys: KeyDefinition[]): void { + this._initConfig(keys, this.keyStates); } // Internal: get ordered key states for a specific provider getOrderedKeysForProvider(provider: string): KeyState[] { - return this.fallbackOrder + return this.keyOrder .map((id) => this.keyStates.get(id)) .filter( (state): state is KeyState => diff --git a/packages/core/src/models/resolver.ts b/packages/core/src/models/resolver.ts deleted file mode 100644 index 3ada713..0000000 --- a/packages/core/src/models/resolver.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { ResolvedModel } from "../types/index.js"; -import type { ModelRegistry } from "./registry.js"; - -export class ModelResolver { - private registry: ModelRegistry; - - constructor(registry: ModelRegistry) { - this.registry = registry; - } - - resolve(tag: string): ResolvedModel | null { - const models = this.registry.getModelsByTag(tag); - const keys = this.registry.getKeys(); - - for (const keyState of keys) { - if (keyState.status !== "active") continue; - const model = models.find((m) => m.provider === keyState.definition.provider); - if (model) { - return { model, key: keyState.definition }; - } - } - - return null; - } - - async waitForKey( - tag: string, - options?: { - pollIntervalMs?: number; - signal?: AbortSignal; - onWaiting?: () => void; - onResume?: () => void; - }, - ): Promise<ResolvedModel | null> { - const pollIntervalMs = options?.pollIntervalMs ?? 60000; - const signal = options?.signal; - - // Try immediately first - const immediate = this.resolve(tag); - if (immediate) return immediate; - - // Check if aborted before entering wait state - if (signal?.aborted) return null; - - options?.onWaiting?.(); - - return new Promise<ResolvedModel | null>((resolve) => { - let timer: ReturnType<typeof setTimeout> | null = null; - - const cleanup = () => { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - }; - - const onAbort = () => { - cleanup(); - resolve(null); - }; - - if (signal) { - signal.addEventListener("abort", onAbort, { once: true }); - } - - const poll = () => { - if (signal?.aborted) { - resolve(null); - return; - } - - const result = this.resolve(tag); - if (result) { - if (signal) { - signal.removeEventListener("abort", onAbort); - } - options?.onResume?.(); - resolve(result); - return; - } - - timer = setTimeout(poll, pollIntervalMs); - }; - - timer = setTimeout(poll, pollIntervalMs); - }); - } -} diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 6977564..f5dafbb 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -77,28 +77,10 @@ export interface AgentConfig { // ─── Config Types (dispatch.toml) ──────────────────────────────── export interface DispatchConfig { - agents?: Record<string, AgentTemplate>; - models?: ModelDefinition[]; keys?: KeyDefinition[]; - fallback?: string[]; permissions: Record<string, string | Record<string, string>>; } -export interface AgentTemplate { - name: string; - description: string; - system_prompt: string; - tools: string[]; - permissions: Record<string, string | Record<string, string>>; - model_tag: string; -} - -export interface ModelDefinition { - id: string; - provider: string; - tags: string[]; -} - export interface KeyDefinition { id: string; provider: string; @@ -108,13 +90,6 @@ export interface KeyDefinition { credentials_file?: string; } -// ─── Model Resolution ──────────────────────────────────────────── - -export interface ResolvedModel { - model: ModelDefinition; - key: KeyDefinition; -} - export type KeyStatus = "active" | "exhausted"; export interface KeyState { |
