feat(models): add background task model override config and runtime wiring
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { ModelRouter, ModelTier } from '../../models/router.js';
|
import type { ModelRouter, ModelTier } from '../../models/router.js';
|
||||||
import type { ChatRequest, Message, TokenUsage } from '../../models/types.js';
|
import type { ChatRequest, Message, ModelClient, TokenUsage } from '../../models/types.js';
|
||||||
import type { Session } from '../../session/index.js';
|
import type { Session } from '../../session/index.js';
|
||||||
import type { ToolRegistry } from '../../tools/registry.js';
|
import type { ToolRegistry } from '../../tools/registry.js';
|
||||||
import type { ToolExecutor } from '../../tools/executor.js';
|
import type { ToolExecutor } from '../../tools/executor.js';
|
||||||
@@ -22,6 +22,7 @@ import { CONTEXT_CHECKPOINT_PROMPT, MEMORY_EXTRACTION_PROMPT } from './prompts.j
|
|||||||
/** A single-turn, stateless request to a sub-agent at a specific tier. */
|
/** A single-turn, stateless request to a sub-agent at a specific tier. */
|
||||||
export interface SubAgentRequest {
|
export interface SubAgentRequest {
|
||||||
tier: ModelTier;
|
tier: ModelTier;
|
||||||
|
task?: keyof DelegationConfig;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
message: string;
|
message: string;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
@@ -106,6 +107,12 @@ export interface OrchestratorConfig {
|
|||||||
primaryTier: ModelTier;
|
primaryTier: ModelTier;
|
||||||
/** Which tier to use for each delegation task type. */
|
/** Which tier to use for each delegation task type. */
|
||||||
delegation: DelegationConfig;
|
delegation: DelegationConfig;
|
||||||
|
/** Optional direct provider/model overrides for specific delegation task types. */
|
||||||
|
backgroundModelOverrides?: Partial<Record<keyof DelegationConfig, {
|
||||||
|
client: ModelClient;
|
||||||
|
label: string;
|
||||||
|
fallbackTier: ModelTier;
|
||||||
|
}>>;
|
||||||
/** Maximum nesting depth for delegation calls (safety guard). */
|
/** Maximum nesting depth for delegation calls (safety guard). */
|
||||||
maxDelegationDepth: number;
|
maxDelegationDepth: number;
|
||||||
onToolUse?: (event: ToolUseEvent) => void;
|
onToolUse?: (event: ToolUseEvent) => void;
|
||||||
@@ -157,6 +164,11 @@ export class AgentOrchestrator {
|
|||||||
private _agent: NativeAgent;
|
private _agent: NativeAgent;
|
||||||
private _modelRouter: ModelRouter;
|
private _modelRouter: ModelRouter;
|
||||||
private _delegation: DelegationConfig;
|
private _delegation: DelegationConfig;
|
||||||
|
private _backgroundModelOverrides: Partial<Record<keyof DelegationConfig, {
|
||||||
|
client: ModelClient;
|
||||||
|
label: string;
|
||||||
|
fallbackTier: ModelTier;
|
||||||
|
}>>;
|
||||||
private _maxDelegationDepth: number;
|
private _maxDelegationDepth: number;
|
||||||
private _toolRegistry?: ToolRegistry;
|
private _toolRegistry?: ToolRegistry;
|
||||||
private _session?: Session;
|
private _session?: Session;
|
||||||
@@ -185,6 +197,7 @@ export class AgentOrchestrator {
|
|||||||
constructor(config: OrchestratorConfig) {
|
constructor(config: OrchestratorConfig) {
|
||||||
this._modelRouter = config.modelRouter;
|
this._modelRouter = config.modelRouter;
|
||||||
this._delegation = config.delegation;
|
this._delegation = config.delegation;
|
||||||
|
this._backgroundModelOverrides = config.backgroundModelOverrides ?? {};
|
||||||
this._maxDelegationDepth = config.maxDelegationDepth;
|
this._maxDelegationDepth = config.maxDelegationDepth;
|
||||||
this._toolRegistry = config.toolRegistry;
|
this._toolRegistry = config.toolRegistry;
|
||||||
this._session = config.session;
|
this._session = config.session;
|
||||||
@@ -262,6 +275,30 @@ export class AgentOrchestrator {
|
|||||||
chatRequest.tools = this._toolRegistry.filteredToAnthropicFormat(policyContext);
|
chatRequest.tools = this._toolRegistry.filteredToAnthropicFormat(policyContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const override = request.task ? this._backgroundModelOverrides[request.task] : undefined;
|
||||||
|
if (override) {
|
||||||
|
try {
|
||||||
|
const response = await override.client.chat(chatRequest);
|
||||||
|
this._trackUsage(tier, response.usage);
|
||||||
|
console.log(
|
||||||
|
`[Flynn:delegate] task=${request.task} provider_model=${override.label} fallback_tier=${override.fallbackTier} ` +
|
||||||
|
`tokens=${response.usage.inputTokens}+${response.usage.outputTokens}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
content: response.content,
|
||||||
|
usage: response.usage,
|
||||||
|
tier,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.warn(
|
||||||
|
`[Flynn:delegate] task=${request.task} provider/model override failed (${override.label}): ${message}; ` +
|
||||||
|
`falling back to tier='${override.fallbackTier}'`,
|
||||||
|
);
|
||||||
|
tier = override.fallbackTier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this._modelRouter.chat(chatRequest, tier);
|
const response = await this._modelRouter.chat(chatRequest, tier);
|
||||||
|
|
||||||
// Track cumulative usage for this tier
|
// Track cumulative usage for this tier
|
||||||
@@ -645,6 +682,7 @@ export class AgentOrchestrator {
|
|||||||
try {
|
try {
|
||||||
const extractionTier = this.getDelegationTier('memory_extraction');
|
const extractionTier = this.getDelegationTier('memory_extraction');
|
||||||
const extraction = await this.delegate({
|
const extraction = await this.delegate({
|
||||||
|
task: 'memory_extraction',
|
||||||
tier: extractionTier,
|
tier: extractionTier,
|
||||||
systemPrompt: MEMORY_EXTRACTION_PROMPT,
|
systemPrompt: MEMORY_EXTRACTION_PROMPT,
|
||||||
message: this._buildExtractionInput(userMessage, assistantText, toolCallsInRun),
|
message: this._buildExtractionInput(userMessage, assistantText, toolCallsInRun),
|
||||||
@@ -861,6 +899,7 @@ export class AgentOrchestrator {
|
|||||||
try {
|
try {
|
||||||
const tier = this.getDelegationTier('compaction');
|
const tier = this.getDelegationTier('compaction');
|
||||||
const result = await this.delegate({
|
const result = await this.delegate({
|
||||||
|
task: 'compaction',
|
||||||
tier,
|
tier,
|
||||||
systemPrompt: CONTEXT_CHECKPOINT_PROMPT,
|
systemPrompt: CONTEXT_CHECKPOINT_PROMPT,
|
||||||
message: conversation,
|
message: conversation,
|
||||||
|
|||||||
@@ -490,6 +490,38 @@ const agentsSchema = z.object({
|
|||||||
tool_summarisation: 'fast',
|
tool_summarisation: 'fast',
|
||||||
complex_reasoning: 'complex',
|
complex_reasoning: 'complex',
|
||||||
}),
|
}),
|
||||||
|
background_models: z.object({
|
||||||
|
compaction: z.object({
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
provider: z.enum(MODEL_PROVIDERS),
|
||||||
|
model: z.string().min(1),
|
||||||
|
fallback_tier: modelTierEnum.default('fast'),
|
||||||
|
}).optional(),
|
||||||
|
memory_extraction: z.object({
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
provider: z.enum(MODEL_PROVIDERS),
|
||||||
|
model: z.string().min(1),
|
||||||
|
fallback_tier: modelTierEnum.default('fast'),
|
||||||
|
}).optional(),
|
||||||
|
classification: z.object({
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
provider: z.enum(MODEL_PROVIDERS),
|
||||||
|
model: z.string().min(1),
|
||||||
|
fallback_tier: modelTierEnum.default('fast'),
|
||||||
|
}).optional(),
|
||||||
|
tool_summarisation: z.object({
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
provider: z.enum(MODEL_PROVIDERS),
|
||||||
|
model: z.string().min(1),
|
||||||
|
fallback_tier: modelTierEnum.default('fast'),
|
||||||
|
}).optional(),
|
||||||
|
complex_reasoning: z.object({
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
provider: z.enum(MODEL_PROVIDERS),
|
||||||
|
model: z.string().min(1),
|
||||||
|
fallback_tier: modelTierEnum.default('fast'),
|
||||||
|
}).optional(),
|
||||||
|
}).default({}),
|
||||||
auto_escalate: z.boolean().default(false),
|
auto_escalate: z.boolean().default(false),
|
||||||
max_delegation_depth: z.number().min(1).max(10).default(3),
|
max_delegation_depth: z.number().min(1).max(10).default(3),
|
||||||
/** Maximum tool-loop iterations before the agent stops. */
|
/** Maximum tool-loop iterations before the agent stops. */
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export async function compactHistory(opts: {
|
|||||||
const tier = orchestrator.getDelegationTier('compaction');
|
const tier = orchestrator.getDelegationTier('compaction');
|
||||||
|
|
||||||
const result = await orchestrator.delegate({
|
const result = await orchestrator.delegate({
|
||||||
|
task: 'compaction',
|
||||||
tier,
|
tier,
|
||||||
systemPrompt: COMPACTION_SYSTEM_PROMPT,
|
systemPrompt: COMPACTION_SYSTEM_PROMPT,
|
||||||
message: formattedConversation,
|
message: formattedConversation,
|
||||||
@@ -133,6 +134,7 @@ export async function compactHistory(opts: {
|
|||||||
try {
|
try {
|
||||||
const extractionTier = orchestrator.getDelegationTier('memory_extraction');
|
const extractionTier = orchestrator.getDelegationTier('memory_extraction');
|
||||||
const extraction = await orchestrator.delegate({
|
const extraction = await orchestrator.delegate({
|
||||||
|
task: 'memory_extraction',
|
||||||
tier: extractionTier,
|
tier: extractionTier,
|
||||||
systemPrompt: MEMORY_EXTRACTION_PROMPT,
|
systemPrompt: MEMORY_EXTRACTION_PROMPT,
|
||||||
message: `Extract persistent facts from this conversation:\n\n${formattedConversation}`,
|
message: `Extract persistent facts from this conversation:\n\n${formattedConversation}`,
|
||||||
|
|||||||
+65
-1
@@ -13,7 +13,7 @@ import { createMediaSendTool, createAgentDelegateTool } from '../tools/index.js'
|
|||||||
import type { AgentDelegateDeps } from '../tools/index.js';
|
import type { AgentDelegateDeps } from '../tools/index.js';
|
||||||
import { createSandboxedShellTool, createSandboxedProcessStartTool, SandboxManager } from '../sandbox/index.js';
|
import { createSandboxedShellTool, createSandboxedProcessStartTool, SandboxManager } from '../sandbox/index.js';
|
||||||
import { MODEL_PROVIDERS, type Config, type ModelConfig, type ModelProvider } from '../config/index.js';
|
import { MODEL_PROVIDERS, type Config, type ModelConfig, type ModelProvider } from '../config/index.js';
|
||||||
import { ModelRouter, type ModelTier } from '../models/index.js';
|
import { ModelRouter, type ModelClient, type ModelTier } from '../models/index.js';
|
||||||
import { ToolRegistry, ToolExecutor } from '../tools/index.js';
|
import { ToolRegistry, ToolExecutor } from '../tools/index.js';
|
||||||
import { SessionManager } from '../session/index.js';
|
import { SessionManager } from '../session/index.js';
|
||||||
import { AgentConfigRegistry, AgentRouter } from '../agents/index.js';
|
import { AgentConfigRegistry, AgentRouter } from '../agents/index.js';
|
||||||
@@ -77,6 +77,55 @@ function tierFromUseCase(config: Config, useCaseRaw: unknown): ModelTier | undef
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildBackgroundModelOverrides(config: Config): Partial<Record<keyof DelegationConfig, {
|
||||||
|
client: ModelClient;
|
||||||
|
label: string;
|
||||||
|
fallbackTier: ModelTier;
|
||||||
|
}>> {
|
||||||
|
const overrides: Partial<Record<keyof DelegationConfig, {
|
||||||
|
client: ModelClient;
|
||||||
|
label: string;
|
||||||
|
fallbackTier: ModelTier;
|
||||||
|
}>> = {};
|
||||||
|
const configured = config.agents?.background_models ?? {};
|
||||||
|
const providerConfigs = buildProviderConfigMap(config);
|
||||||
|
const tasks: Array<keyof DelegationConfig> = [
|
||||||
|
'compaction',
|
||||||
|
'memory_extraction',
|
||||||
|
'classification',
|
||||||
|
'tool_summarisation',
|
||||||
|
'complex_reasoning',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
const entry = configured[task];
|
||||||
|
if (!entry || entry.enabled === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = providerConfigs[entry.provider];
|
||||||
|
try {
|
||||||
|
const client = createClientFromConfig(
|
||||||
|
template
|
||||||
|
? { ...template, provider: entry.provider, model: entry.model }
|
||||||
|
: { provider: entry.provider, model: entry.model },
|
||||||
|
);
|
||||||
|
overrides[task] = {
|
||||||
|
client,
|
||||||
|
label: `${entry.provider}/${entry.model}`,
|
||||||
|
fallbackTier: entry.fallback_tier,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`[Flynn:routing] Failed to initialize background model override for ${task} ` +
|
||||||
|
`(${entry.provider}/${entry.model}): ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
function parseResearchPrefix(text: string): string | undefined {
|
function parseResearchPrefix(text: string): string | undefined {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
const researchMatch = trimmed.match(/^research(?:\s*[:,-])?\s+(.+)$/i);
|
const researchMatch = trimmed.match(/^research(?:\s*[:,-])?\s+(.+)$/i);
|
||||||
@@ -174,6 +223,7 @@ export function createMessageRouter(deps: {
|
|||||||
// Cache agents by session ID + agent config name to avoid recreating on every message
|
// Cache agents by session ID + agent config name to avoid recreating on every message
|
||||||
const agents = new Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }>();
|
const agents = new Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }>();
|
||||||
const talkModeUntil = new Map<string, number>();
|
const talkModeUntil = new Map<string, number>();
|
||||||
|
const activeRuns = new Map<string, AgentOrchestrator>();
|
||||||
|
|
||||||
async function maybeBuildTtsAttachment(responseText: string, channel: string) {
|
async function maybeBuildTtsAttachment(responseText: string, channel: string) {
|
||||||
if (!isTtsEnabledForChannel(deps.config, channel)) {
|
if (!isTtsEnabledForChannel(deps.config, channel)) {
|
||||||
@@ -261,6 +311,7 @@ export function createMessageRouter(deps: {
|
|||||||
tool_summarisation: deps.config.agents.delegation.tool_summarisation ?? 'fast',
|
tool_summarisation: deps.config.agents.delegation.tool_summarisation ?? 'fast',
|
||||||
complex_reasoning: deps.config.agents.delegation.complex_reasoning ?? 'complex',
|
complex_reasoning: deps.config.agents.delegation.complex_reasoning ?? 'complex',
|
||||||
};
|
};
|
||||||
|
const backgroundModelOverrides = buildBackgroundModelOverrides(deps.config);
|
||||||
|
|
||||||
// Clone the tool registry and replace high-risk tools with sandboxed versions if configured.
|
// Clone the tool registry and replace high-risk tools with sandboxed versions if configured.
|
||||||
let effectiveToolRegistry = deps.toolRegistry;
|
let effectiveToolRegistry = deps.toolRegistry;
|
||||||
@@ -377,6 +428,7 @@ export function createMessageRouter(deps: {
|
|||||||
toolExecutor: deps.toolExecutor,
|
toolExecutor: deps.toolExecutor,
|
||||||
primaryTier: effectiveTier,
|
primaryTier: effectiveTier,
|
||||||
delegation: delegationConfig,
|
delegation: delegationConfig,
|
||||||
|
backgroundModelOverrides,
|
||||||
maxDelegationDepth: deps.config.agents.max_delegation_depth ?? 3,
|
maxDelegationDepth: deps.config.agents.max_delegation_depth ?? 3,
|
||||||
maxIterations: deps.config.agents.max_iterations,
|
maxIterations: deps.config.agents.max_iterations,
|
||||||
compaction: deps.config.compaction.enabled ? {
|
compaction: deps.config.compaction.enabled ? {
|
||||||
@@ -419,6 +471,7 @@ export function createMessageRouter(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handler = async (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>): Promise<void> => {
|
const handler = async (msg: InboundMessage, reply: (response: OutboundMessage) => Promise<void>): Promise<void> => {
|
||||||
|
const sessionIdForRun = `${msg.channel}:${msg.senderId}`;
|
||||||
let incomingText = msg.text;
|
let incomingText = msg.text;
|
||||||
let matchedReactionName: string | undefined;
|
let matchedReactionName: string | undefined;
|
||||||
const talkMode = deps.config.audio?.talk_mode;
|
const talkMode = deps.config.audio?.talk_mode;
|
||||||
@@ -721,6 +774,14 @@ export function createMessageRouter(deps: {
|
|||||||
session.deleteConfig('modelTier');
|
session.deleteConfig('modelTier');
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
cancelRun: () => {
|
||||||
|
const run = activeRuns.get(session.id);
|
||||||
|
if (!run || !run.isCancellable()) {
|
||||||
|
return 'No active operation to cancel.';
|
||||||
|
}
|
||||||
|
run.cancel();
|
||||||
|
return 'Cancellation requested. The active operation will stop at the next safe point.';
|
||||||
|
},
|
||||||
|
|
||||||
delegateAgent: async (agentName: string, task: string) => {
|
delegateAgent: async (agentName: string, task: string) => {
|
||||||
const target = agentName.trim();
|
const target = agentName.trim();
|
||||||
@@ -1293,6 +1354,7 @@ export function createMessageRouter(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let response: string;
|
let response: string;
|
||||||
|
activeRuns.set(sessionIdForRun, agent);
|
||||||
try {
|
try {
|
||||||
response = await agent.process(messageText, attachments);
|
response = await agent.process(messageText, attachments);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1322,6 +1384,8 @@ export function createMessageRouter(deps: {
|
|||||||
text: 'Sorry, an error occurred while processing your message.',
|
text: 'Sorry, an error occurred while processing your message.',
|
||||||
replyTo: msg.id,
|
replyTo: msg.id,
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
activeRuns.delete(sessionIdForRun);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
|
||||||
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
|
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
|
||||||
import type { Config } from '../../config/index.js';
|
import { MODEL_PROVIDERS, type Config, type ModelProvider } from '../../config/index.js';
|
||||||
|
|
||||||
export interface ConfigHandlerDeps {
|
export interface ConfigHandlerDeps {
|
||||||
config: Config;
|
config: Config;
|
||||||
@@ -163,6 +163,156 @@ const PATCHABLE_KEYS: Record<string, (config: Config, value: unknown) => boolean
|
|||||||
config.server.nodes.push.enabled = value;
|
config.server.nodes.push.enabled = value;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
'agents.primary_tier': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.primary_tier = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.delegation.compaction': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.delegation.compaction = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.delegation.memory_extraction': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.delegation.memory_extraction = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.delegation.classification': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.delegation.classification = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.delegation.tool_summarisation': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.delegation.tool_summarisation = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.delegation.complex_reasoning': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.delegation.complex_reasoning = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.compaction.enabled': (config, value) => {
|
||||||
|
if (typeof value !== 'boolean') {return false;}
|
||||||
|
config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.compaction.enabled = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.compaction.provider': (config, value) => {
|
||||||
|
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
|
||||||
|
config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.compaction.provider = value as ModelProvider;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.compaction.model': (config, value) => {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
|
||||||
|
config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.compaction.model = value.trim();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.compaction.fallback_tier': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.background_models.compaction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.compaction.fallback_tier = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.memory_extraction.enabled': (config, value) => {
|
||||||
|
if (typeof value !== 'boolean') {return false;}
|
||||||
|
config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.memory_extraction.enabled = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.memory_extraction.provider': (config, value) => {
|
||||||
|
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
|
||||||
|
config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.memory_extraction.provider = value as ModelProvider;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.memory_extraction.model': (config, value) => {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
|
||||||
|
config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.memory_extraction.model = value.trim();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.memory_extraction.fallback_tier': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.background_models.memory_extraction ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.memory_extraction.fallback_tier = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.classification.enabled': (config, value) => {
|
||||||
|
if (typeof value !== 'boolean') {return false;}
|
||||||
|
config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.classification.enabled = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.classification.provider': (config, value) => {
|
||||||
|
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
|
||||||
|
config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.classification.provider = value as ModelProvider;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.classification.model': (config, value) => {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
|
||||||
|
config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.classification.model = value.trim();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.classification.fallback_tier': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.background_models.classification ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.classification.fallback_tier = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.tool_summarisation.enabled': (config, value) => {
|
||||||
|
if (typeof value !== 'boolean') {return false;}
|
||||||
|
config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.tool_summarisation.enabled = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.tool_summarisation.provider': (config, value) => {
|
||||||
|
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
|
||||||
|
config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.tool_summarisation.provider = value as ModelProvider;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.tool_summarisation.model': (config, value) => {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
|
||||||
|
config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.tool_summarisation.model = value.trim();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.tool_summarisation.fallback_tier': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.background_models.tool_summarisation ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.tool_summarisation.fallback_tier = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.complex_reasoning.enabled': (config, value) => {
|
||||||
|
if (typeof value !== 'boolean') {return false;}
|
||||||
|
config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.complex_reasoning.enabled = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.complex_reasoning.provider': (config, value) => {
|
||||||
|
if (!MODEL_PROVIDERS.includes(String(value) as ModelProvider)) {return false;}
|
||||||
|
config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.complex_reasoning.provider = value as ModelProvider;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.complex_reasoning.model': (config, value) => {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) {return false;}
|
||||||
|
config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.complex_reasoning.model = value.trim();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'agents.background_models.complex_reasoning.fallback_tier': (config, value) => {
|
||||||
|
if (value !== 'fast' && value !== 'default' && value !== 'complex' && value !== 'local') {return false;}
|
||||||
|
config.agents.background_models.complex_reasoning ??= { enabled: true, provider: 'openai', model: 'gpt-4o-mini', fallback_tier: 'fast' };
|
||||||
|
config.agents.background_models.complex_reasoning.fallback_tier = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
'automation.delivery_mode': (config, value) => {
|
'automation.delivery_mode': (config, value) => {
|
||||||
if (value !== 'shared_session' && value !== 'isolated_job' && value !== 'announce') {return false;}
|
if (value !== 'shared_session' && value !== 'isolated_job' && value !== 'announce') {return false;}
|
||||||
config.automation ??= {} as Config['automation'];
|
config.automation ??= {} as Config['automation'];
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { randomUUID } from 'crypto';
|
|||||||
import type { SessionManager } from '../session/manager.js';
|
import type { SessionManager } from '../session/manager.js';
|
||||||
import type { ModelClient } from '../models/types.js';
|
import type { ModelClient } from '../models/types.js';
|
||||||
import type { ModelRouter, ModelTier } from '../models/router.js';
|
import type { ModelRouter, ModelTier } from '../models/router.js';
|
||||||
|
import { createClientFromConfig } from '../daemon/models.js';
|
||||||
|
import type { Config, ModelConfig, ModelProvider } from '../config/index.js';
|
||||||
import type { ToolRegistry } from '../tools/registry.js';
|
import type { ToolRegistry } from '../tools/registry.js';
|
||||||
import type { ToolExecutor } from '../tools/executor.js';
|
import type { ToolExecutor } from '../tools/executor.js';
|
||||||
import { AgentOrchestrator, type DelegationConfig } from '../backends/native/orchestrator.js';
|
import { AgentOrchestrator, type DelegationConfig } from '../backends/native/orchestrator.js';
|
||||||
import type { ToolUseEvent } from '../backends/native/agent.js';
|
import type { ToolUseEvent } from '../backends/native/agent.js';
|
||||||
import type { MemoryStore } from '../memory/store.js';
|
import type { MemoryStore } from '../memory/store.js';
|
||||||
import type { Config } from '../config/index.js';
|
|
||||||
import { summarizeSessionOnEnd, type SessionEndSummaryConfig } from '../session/endSummary.js';
|
import { summarizeSessionOnEnd, type SessionEndSummaryConfig } from '../session/endSummary.js';
|
||||||
|
|
||||||
export interface SessionBridgeConfig {
|
export interface SessionBridgeConfig {
|
||||||
@@ -284,6 +285,7 @@ export class SessionBridge {
|
|||||||
tool_summarisation: config?.agents.delegation.tool_summarisation ?? 'fast',
|
tool_summarisation: config?.agents.delegation.tool_summarisation ?? 'fast',
|
||||||
complex_reasoning: config?.agents.delegation.complex_reasoning ?? 'complex',
|
complex_reasoning: config?.agents.delegation.complex_reasoning ?? 'complex',
|
||||||
};
|
};
|
||||||
|
const backgroundModelOverrides = this.buildBackgroundModelOverrides();
|
||||||
|
|
||||||
agent = new AgentOrchestrator({
|
agent = new AgentOrchestrator({
|
||||||
modelRouter: this.config.modelClient as ModelRouter,
|
modelRouter: this.config.modelClient as ModelRouter,
|
||||||
@@ -293,6 +295,7 @@ export class SessionBridge {
|
|||||||
toolExecutor: this.config.toolExecutor,
|
toolExecutor: this.config.toolExecutor,
|
||||||
primaryTier,
|
primaryTier,
|
||||||
delegation: delegationConfig,
|
delegation: delegationConfig,
|
||||||
|
backgroundModelOverrides,
|
||||||
maxDelegationDepth: config?.agents.max_delegation_depth ?? 3,
|
maxDelegationDepth: config?.agents.max_delegation_depth ?? 3,
|
||||||
maxIterations: config?.agents.max_iterations,
|
maxIterations: config?.agents.max_iterations,
|
||||||
compaction: config?.compaction.enabled ? {
|
compaction: config?.compaction.enabled ? {
|
||||||
@@ -337,4 +340,77 @@ export class SessionBridge {
|
|||||||
}
|
}
|
||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildBackgroundModelOverrides(): Partial<Record<keyof DelegationConfig, {
|
||||||
|
client: ModelClient;
|
||||||
|
label: string;
|
||||||
|
fallbackTier: ModelTier;
|
||||||
|
}>> {
|
||||||
|
const runtimeConfig = this.config.config;
|
||||||
|
const overrides: Partial<Record<keyof DelegationConfig, {
|
||||||
|
client: ModelClient;
|
||||||
|
label: string;
|
||||||
|
fallbackTier: ModelTier;
|
||||||
|
}>> = {};
|
||||||
|
if (!runtimeConfig) {
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerConfigs = this.buildProviderConfigMap(runtimeConfig);
|
||||||
|
const configured = runtimeConfig.agents?.background_models ?? {};
|
||||||
|
const tasks: Array<keyof DelegationConfig> = [
|
||||||
|
'compaction',
|
||||||
|
'memory_extraction',
|
||||||
|
'classification',
|
||||||
|
'tool_summarisation',
|
||||||
|
'complex_reasoning',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
const entry = configured[task];
|
||||||
|
if (!entry || entry.enabled === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const template = providerConfigs[entry.provider];
|
||||||
|
try {
|
||||||
|
const client = createClientFromConfig(
|
||||||
|
template
|
||||||
|
? { ...template, provider: entry.provider, model: entry.model }
|
||||||
|
: { provider: entry.provider, model: entry.model },
|
||||||
|
);
|
||||||
|
overrides[task] = {
|
||||||
|
client,
|
||||||
|
label: `${entry.provider}/${entry.model}`,
|
||||||
|
fallbackTier: entry.fallback_tier,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`[Flynn:gateway] Failed to initialize background model override for ${task} ` +
|
||||||
|
`(${entry.provider}/${entry.model}): ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildProviderConfigMap(config: Config): Partial<Record<ModelProvider, ModelConfig>> {
|
||||||
|
const providerConfigs: Partial<Record<ModelProvider, ModelConfig>> = {};
|
||||||
|
const modelConfigs: ModelConfig[] = [
|
||||||
|
config.models.default,
|
||||||
|
...(config.models.fast ? [config.models.fast] : []),
|
||||||
|
...(config.models.complex ? [config.models.complex] : []),
|
||||||
|
...(config.models.local ? [config.models.local] : []),
|
||||||
|
...Object.values(config.models.local_providers ?? {}),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const modelConfig of modelConfigs) {
|
||||||
|
providerConfigs[modelConfig.provider] = modelConfig;
|
||||||
|
if (modelConfig.fallback) {
|
||||||
|
providerConfigs[modelConfig.fallback.provider] = modelConfig.fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerConfigs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user