feat(skills): enable watcher wiring through daemon lifecycle
This commit is contained in:
+26
-4
@@ -19,6 +19,9 @@ import { initAgents } from './agents.js';
|
||||
import { createMessageRouter } from './routing.js';
|
||||
import { registerChannels } from './channels.js';
|
||||
import { initSkills, initMcp, loadSystemPrompt, initPairingManager, createGateway, startServices } from './services.js';
|
||||
import { CommandRegistry, registerBuiltinCommands } from '../commands/index.js';
|
||||
import { ComponentRegistry } from '../intents/index.js';
|
||||
import { RoutingPolicy } from '../routing/index.js';
|
||||
|
||||
// ── Infrastructure ──
|
||||
import type { ModelRouter } from '../models/index.js';
|
||||
@@ -71,7 +74,12 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
});
|
||||
|
||||
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
|
||||
const sessionManager = new SessionManager(sessionStore);
|
||||
const sessionManager = new SessionManager(sessionStore, {
|
||||
enabled: config.history_index.enabled,
|
||||
maxKeywords: config.history_index.max_keywords,
|
||||
searchLimit: config.history_index.search_limit,
|
||||
minScore: config.history_index.min_score,
|
||||
});
|
||||
|
||||
lifecycle.onShutdown(async () => {
|
||||
sessionStore.close();
|
||||
@@ -96,10 +104,24 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
const { toolRegistry, toolExecutor, browserManager } = initTools({ config, lifecycle, hookEngine });
|
||||
const { memoryStore, memoryDir } = await initMemory({ config, dataDir, lifecycle, toolRegistry });
|
||||
const mcpManager = await initMcp(config, lifecycle, toolRegistry);
|
||||
const { skillRegistry, skillInstaller } = initSkills(config);
|
||||
const { skillRegistry, skillInstaller } = initSkills(config, lifecycle);
|
||||
const { agentConfigRegistry, agentRouter, sandboxManager } = await initAgents({ config, lifecycle });
|
||||
|
||||
const modelRouter = createModelRouter(config);
|
||||
const commandRegistry = new CommandRegistry();
|
||||
registerBuiltinCommands(commandRegistry);
|
||||
const intentRegistry = new ComponentRegistry({
|
||||
matchThreshold: config.intents.match_threshold,
|
||||
});
|
||||
if (config.intents.enabled) {
|
||||
intentRegistry.loadRules(config.intents.rules);
|
||||
}
|
||||
const routingPolicy = new RoutingPolicy({
|
||||
enabled: config.routing_policy.enabled,
|
||||
fastPathThreshold: config.routing_policy.fast_path_threshold,
|
||||
llmThreshold: config.routing_policy.llm_threshold,
|
||||
defaultPath: config.routing_policy.default_path,
|
||||
});
|
||||
|
||||
// Restore persisted model tier
|
||||
const { loadPreferences, savePreference } = await import('../preferences.js');
|
||||
@@ -121,12 +143,12 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||
const gateway = createGateway({
|
||||
config, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor,
|
||||
channelRegistry, pairingManager, lifecycle, memoryStore,
|
||||
getChannelAgents: () => channelAgents,
|
||||
getChannelAgents: () => channelAgents, commandRegistry, intentRegistry, routingPolicy,
|
||||
});
|
||||
|
||||
const messageRouter = createMessageRouter({
|
||||
sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor,
|
||||
config, memoryStore, agentConfigRegistry, agentRouter, sandboxManager,
|
||||
config, memoryStore, agentConfigRegistry, agentRouter, sandboxManager, commandRegistry, intentRegistry, routingPolicy,
|
||||
});
|
||||
channelRegistry.setMessageHandler(messageRouter.handler);
|
||||
channelAgents = messageRouter.agents;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { configSchema } from '../config/schema.js';
|
||||
import { Lifecycle } from './lifecycle.js';
|
||||
import { initSkills } from './services.js';
|
||||
|
||||
describe('initSkills watcher wiring', () => {
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeConfig(overrides: Record<string, unknown> = {}) {
|
||||
return configSchema.parse({
|
||||
telegram: { bot_token: 'test-token', allowed_chat_ids: [1] },
|
||||
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
it('does not create a watcher when disabled', () => {
|
||||
const config = makeConfig();
|
||||
const lifecycle = new Lifecycle();
|
||||
|
||||
const result = initSkills(config, lifecycle);
|
||||
|
||||
expect(result.skillsWatcher).toBeUndefined();
|
||||
});
|
||||
|
||||
it('starts watcher and stops it on lifecycle shutdown when enabled', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'flynn-services-'));
|
||||
roots.push(root);
|
||||
const managedDir = join(root, 'skills');
|
||||
mkdirSync(managedDir, { recursive: true });
|
||||
|
||||
const config = makeConfig({
|
||||
skills: {
|
||||
managed_dir: managedDir,
|
||||
load: { watch: true, watch_debounce_ms: 100 },
|
||||
},
|
||||
});
|
||||
const lifecycle = new Lifecycle();
|
||||
|
||||
const result = initSkills(config, lifecycle);
|
||||
|
||||
expect(result.skillsWatcher?.isRunning).toBe(true);
|
||||
expect(result.skillsWatcher?.watchedDirectoryCount).toBe(1);
|
||||
|
||||
await lifecycle.shutdown();
|
||||
expect(result.skillsWatcher?.isRunning).toBe(false);
|
||||
});
|
||||
});
|
||||
+34
-3
@@ -9,7 +9,7 @@ import { GatewayServer } from '../gateway/index.js';
|
||||
import { ChannelRegistry, PairingManager, type PairingStore } from '../channels/index.js';
|
||||
import { HeartbeatMonitor } from '../automation/index.js';
|
||||
import { McpManager } from '../mcp/index.js';
|
||||
import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js';
|
||||
import { SkillRegistry, SkillInstaller, SkillsWatcher, loadAllSkills } from '../skills/index.js';
|
||||
import { assembleSystemPrompt } from '../prompt/index.js';
|
||||
import { resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
@@ -23,9 +23,10 @@ import type { RoutingPolicy } from '../routing/index.js';
|
||||
export interface SkillsResult {
|
||||
skillRegistry: SkillRegistry;
|
||||
skillInstaller: SkillInstaller;
|
||||
skillsWatcher?: SkillsWatcher;
|
||||
}
|
||||
|
||||
export function initSkills(config: Config): SkillsResult {
|
||||
export function initSkills(config: Config, lifecycle?: Lifecycle): SkillsResult {
|
||||
const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills');
|
||||
const skillRegistry = new SkillRegistry();
|
||||
const skillInstaller = new SkillInstaller(config.skills.managed_dir ?? defaultManagedDir);
|
||||
@@ -45,7 +46,37 @@ export function initSkills(config: Config): SkillsResult {
|
||||
console.log(`Loaded ${skills.length} skill(s) (${available} available)`);
|
||||
}
|
||||
|
||||
return { skillRegistry, skillInstaller };
|
||||
const watchEnabled = config.skills.load.watch;
|
||||
if (!watchEnabled || !lifecycle) {
|
||||
return { skillRegistry, skillInstaller };
|
||||
}
|
||||
|
||||
const skillDirs = [
|
||||
config.skills.bundled_dir,
|
||||
config.skills.managed_dir ?? defaultManagedDir,
|
||||
config.skills.workspace_dir,
|
||||
].filter((dir): dir is string => Boolean(dir));
|
||||
|
||||
const skillsWatcher = new SkillsWatcher({
|
||||
skillDirs,
|
||||
debounceMs: config.skills.load.watch_debounce_ms,
|
||||
onSkillsChanged: ({ changedPaths }) => {
|
||||
console.log(`Skills watcher detected changes in ${changedPaths.length} path(s)`);
|
||||
},
|
||||
});
|
||||
skillsWatcher.start();
|
||||
if (skillsWatcher.watchedDirectoryCount > 0) {
|
||||
console.log(`Skills watcher started (${skillsWatcher.watchedDirectoryCount} dir(s), debounce ${config.skills.load.watch_debounce_ms}ms)`);
|
||||
} else {
|
||||
console.log('Skills watcher enabled, but no existing skill directories to watch');
|
||||
}
|
||||
|
||||
lifecycle.onShutdown(async () => {
|
||||
skillsWatcher.stop();
|
||||
console.log('Skills watcher stopped');
|
||||
});
|
||||
|
||||
return { skillRegistry, skillInstaller, skillsWatcher };
|
||||
}
|
||||
|
||||
// ── MCP ─────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user