summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-21 19:06:15 +0900
committerAdam Malczewski <[email protected]>2026-05-21 19:06:15 +0900
commit55633c90c0d96e62153a4b6655f3f833a3b46ad4 (patch)
treea75f97b48071eb1ba9e8366d8053a376dde69af5 /packages/core/src
parentd6b208342edf97bafa5b1dcc986b782f9879d141 (diff)
downloaddispatch-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.ts126
-rw-r--r--packages/core/src/index.ts2
-rw-r--r--packages/core/src/models/index.ts1
-rw-r--r--packages/core/src/models/registry.ts64
-rw-r--r--packages/core/src/models/resolver.ts88
-rw-r--r--packages/core/src/types/index.ts25
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 {