feat: wire up all Phase 2-6 features into daemon and config

Integrate all new features into the shared infrastructure:
- Config schema: add memory, discord, slack, process, web_search schemas
- Daemon wiring: memory store init, tool registration, channel adapters
- Orchestrator: memory injection into system prompt, extraction on compaction
- Agent: add setSystemPrompt() for dynamic prompt updates
- Channel/tool index: export new adapters and tool factories
- Add @slack/bolt, discord.js, turndown, linkedom, @mozilla/readability deps
- Update state.json with Phase 3b completion (494 tests passing)
This commit is contained in:
William Valentin
2026-02-06 14:24:39 -08:00
parent 6d9e27a591
commit 7a35b22458
12 changed files with 1099 additions and 4 deletions
+4
View File
@@ -199,6 +199,10 @@ export class NativeAgent {
return this.currentTier;
}
setSystemPrompt(prompt: string): void {
this.systemPrompt = prompt;
}
setOnToolUse(callback: ((event: ToolUseEvent) => void) | undefined): void {
this.onToolUse = callback;
}
+30
View File
@@ -3,6 +3,7 @@ import type { ChatRequest, Message, TokenUsage } from '../../models/types.js';
import type { Session } from '../../session/index.js';
import type { ToolRegistry } from '../../tools/registry.js';
import type { ToolExecutor } from '../../tools/executor.js';
import type { MemoryStore } from '../../memory/store.js';
import { NativeAgent } from './agent.js';
import type { ToolUseEvent } from './agent.js';
import { shouldCompact } from '../../context/tokens.js';
@@ -64,6 +65,8 @@ export interface OrchestratorConfig {
modelName?: string;
/** Optional override for the context window size (in tokens). */
contextWindow?: number;
/** Optional memory store for injecting persistent memory into the system prompt. */
memoryStore?: MemoryStore;
}
// ── AgentOrchestrator ─────────────────────────────────────────────────
@@ -86,6 +89,8 @@ export class AgentOrchestrator {
private _compactionConfig?: CompactionConfig;
private _modelName?: string;
private _contextWindow?: number;
private _memoryStore?: MemoryStore;
private _systemPromptBase: string;
private _usageByTier: Map<string, TierUsageStats> = new Map();
constructor(config: OrchestratorConfig) {
@@ -97,6 +102,8 @@ export class AgentOrchestrator {
this._compactionConfig = config.compaction;
this._modelName = config.modelName;
this._contextWindow = config.contextWindow;
this._memoryStore = config.memoryStore;
this._systemPromptBase = config.systemPrompt;
// Create the primary NativeAgent for user-facing conversation
this._agent = new NativeAgent({
@@ -178,6 +185,7 @@ export class AgentOrchestrator {
* exceeds the context window threshold and compacts it before processing.
*/
async process(userMessage: string): Promise<string> {
this._injectMemoryContext();
await this.compactIfNeeded();
return this._agent.process(userMessage);
}
@@ -199,6 +207,7 @@ export class AgentOrchestrator {
messages,
orchestrator: this,
config,
memoryStore: this._memoryStore,
});
// If nothing was actually compacted, skip the replace
@@ -268,6 +277,27 @@ export class AgentOrchestrator {
// ── Private helpers ───────────────────────────────────────────────
/**
* Inject persistent memory context into the primary agent's system prompt.
* Reads from the memory store and appends relevant context to the base
* system prompt. If no memory store is configured or no memory content
* exists, restores the original base prompt.
*/
private _injectMemoryContext(): void {
if (!this._memoryStore) {
return;
}
const memoryContext = this._memoryStore.getContextForPrompt();
if (!memoryContext) {
this._agent.setSystemPrompt(this._systemPromptBase);
return;
}
const enrichedPrompt = `${this._systemPromptBase}\n\n# Memory Context\n\nThe following is your persistent memory. Use it to maintain continuity across sessions.\n\n${memoryContext}`;
this._agent.setSystemPrompt(enrichedPrompt);
}
/**
* Check whether automatic compaction should run, and if so, compact.
* Called before each `process()` call when compaction is configured.
+2
View File
@@ -9,3 +9,5 @@ export type {
export { ChannelRegistry } from './registry.js';
export { TelegramAdapter, type TelegramAdapterConfig } from './telegram/index.js';
export { WebChatAdapter, type WebChatAdapterConfig } from './webchat/index.js';
export { DiscordAdapter, type DiscordAdapterConfig } from './discord/index.js';
export { SlackAdapter, type SlackAdapterConfig } from './slack/index.js';
+44
View File
@@ -107,6 +107,13 @@ const agentsSchema = z.object({
max_delegation_depth: z.number().min(1).max(10).default(3),
}).default({});
const memorySchema = z.object({
enabled: z.boolean().default(true),
dir: z.string().optional(), // Default: ~/.local/share/flynn/memory
auto_extract: z.boolean().default(true),
max_context_tokens: z.number().min(100).max(10000).default(2000),
}).default({});
const compactionSchema = z.object({
enabled: z.boolean().default(true),
threshold_pct: z.number().min(10).max(100).default(80),
@@ -114,8 +121,37 @@ const compactionSchema = z.object({
summary_max_tokens: z.number().min(128).max(4096).default(1024),
}).default({});
const discordSchema = z.object({
bot_token: z.string().min(1, 'Bot token is required'),
allowed_guild_ids: z.array(z.string()).default([]),
allowed_channel_ids: z.array(z.string()).default([]),
require_mention: z.boolean().default(true),
}).optional();
const slackSchema = z.object({
bot_token: z.string().min(1, 'Bot token is required'),
app_token: z.string().min(1, 'App token is required'),
signing_secret: z.string().min(1, 'Signing secret is required'),
allowed_channel_ids: z.array(z.string()).default([]),
}).optional();
const processSchema = z.object({
max_concurrent: z.number().min(1).max(50).default(10),
max_runtime_minutes: z.number().min(1).max(1440).default(60),
buffer_size: z.number().min(1024).max(1048576).default(65536),
}).default({});
const webSearchSchema = z.object({
provider: z.enum(['brave', 'searxng']).default('brave'),
api_key: z.string().optional(),
endpoint: z.string().optional(),
max_results: z.number().min(1).max(20).default(5),
}).default({});
export const configSchema = z.object({
telegram: telegramSchema,
discord: discordSchema,
slack: slackSchema,
server: serverSchema.default({}),
models: modelsSchema,
backends: backendsSchema.default({}),
@@ -125,6 +161,9 @@ export const configSchema = z.object({
automation: automationSchema,
agents: agentsSchema,
compaction: compactionSchema,
memory: memorySchema,
process: processSchema,
web_search: webSearchSchema,
});
export type Config = z.infer<typeof configSchema>;
@@ -133,3 +172,8 @@ export type ModelConfig = z.infer<typeof modelConfigSchema>;
export type CronJobConfig = z.infer<typeof cronJobSchema>;
export type AgentsConfig = z.infer<typeof agentsSchema>;
export type CompactionConfig = z.infer<typeof compactionSchema>;
export type MemoryConfig = z.infer<typeof memorySchema>;
export type WebSearchConfig = z.infer<typeof webSearchSchema>;
export type ProcessConfig = z.infer<typeof processSchema>;
export type DiscordConfig = z.infer<typeof discordSchema>;
export type SlackConfig = z.infer<typeof slackSchema>;
+27 -1
View File
@@ -1,6 +1,7 @@
import type { Message } from '../models/types.js';
import type { AgentOrchestrator } from '../backends/native/orchestrator.js';
import { COMPACTION_SYSTEM_PROMPT } from '../backends/native/prompts.js';
import type { MemoryStore } from '../memory/store.js';
import { COMPACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_PROMPT } from '../backends/native/prompts.js';
import { estimateMessageTokens } from './tokens.js';
export interface CompactionConfig {
@@ -33,6 +34,8 @@ export async function compactHistory(opts: {
messages: Message[];
orchestrator: AgentOrchestrator;
config: CompactionConfig;
memoryStore?: MemoryStore;
autoExtract?: boolean;
}): Promise<CompactionResult> {
const { messages, orchestrator, config } = opts;
@@ -65,6 +68,29 @@ export async function compactHistory(opts: {
content: '[Summary of earlier conversation]\n\n' + result.content,
};
// Phase 2: Extract persistent facts and append to memory (if enabled)
if (opts.memoryStore && opts.autoExtract !== false) {
try {
const extractionTier = orchestrator.getDelegationTier('memory_extraction');
const extraction = await orchestrator.delegate({
tier: extractionTier,
systemPrompt: MEMORY_EXTRACTION_PROMPT,
message: `Extract persistent facts from this conversation:\n\n${formattedConversation}`,
maxTokens: 512,
});
// Only write if the extraction produced meaningful content
const extractedContent = extraction.content.trim();
if (extractedContent.length > 0 && !extractedContent.toLowerCase().includes('no facts')) {
opts.memoryStore.write('global', extractedContent, 'append');
console.log(`[Flynn:memory] Extracted ${extractedContent.length} chars of facts to global memory`);
}
} catch (error) {
// Memory extraction is best-effort — don't fail compaction if it errors
console.warn('[Flynn:memory] Failed to extract facts during compaction:', error);
}
}
return {
messages: [summaryMessage, ...toKeep],
compactedCount: toCompact.length,
+72 -2
View File
@@ -5,9 +5,11 @@ import type { ModelClient } from '../models/index.js';
import { AgentOrchestrator, type DelegationConfig } from '../backends/index.js';
import { SessionStore, SessionManager } from '../session/index.js';
import { HookEngine } from '../hooks/index.js';
import { ToolRegistry, ToolExecutor, allBuiltinTools } from '../tools/index.js';
import { ToolRegistry, ToolExecutor, allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager } from '../tools/index.js';
import { MemoryStore } from '../memory/index.js';
import { createMemoryTools } from '../tools/builtin/index.js';
import { GatewayServer } from '../gateway/index.js';
import { ChannelRegistry, TelegramAdapter, WebChatAdapter } from '../channels/index.js';
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter } from '../channels/index.js';
import { CronScheduler } from '../automation/index.js';
import type { InboundMessage, OutboundMessage } from '../channels/index.js';
import { McpManager } from '../mcp/index.js';
@@ -144,6 +146,7 @@ function createMessageRouter(deps: {
toolRegistry: ToolRegistry;
toolExecutor: ToolExecutor;
config: Config;
memoryStore?: MemoryStore;
}) {
// Cache agents by session ID to avoid recreating on every message
const agents = new Map<string, AgentOrchestrator>();
@@ -176,6 +179,7 @@ function createMessageRouter(deps: {
} : undefined,
modelName: deps.config.models.default.model,
contextWindow: deps.config.models.default.context_window,
memoryStore: deps.memoryStore,
});
agents.set(sessionId, agent);
}
@@ -228,6 +232,13 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
const dataDir = resolve(homedir(), '.local/share/flynn');
mkdirSync(dataDir, { recursive: true });
// Initialize memory store
const memoryDir = config.memory.dir ?? resolve(dataDir, 'memory');
mkdirSync(memoryDir, { recursive: true });
const memoryStore = config.memory.enabled
? new MemoryStore({ dir: memoryDir, maxContextTokens: config.memory.max_context_tokens })
: undefined;
// Initialize session store and manager
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
const sessionManager = new SessionManager(sessionStore);
@@ -245,6 +256,42 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
for (const tool of allBuiltinTools) {
toolRegistry.register(tool);
}
// Register memory tools if memory is enabled
if (memoryStore) {
for (const tool of createMemoryTools(memoryStore)) {
toolRegistry.register(tool);
}
}
// Register web search tool if configured with credentials
if (config.web_search.api_key || config.web_search.endpoint) {
for (const tool of createWebSearchTools({
provider: config.web_search.provider,
apiKey: config.web_search.api_key,
endpoint: config.web_search.endpoint,
maxResults: config.web_search.max_results,
})) {
toolRegistry.register(tool);
}
}
// Initialize process manager and register process tools
const processManager = new ProcessManager({
maxConcurrent: config.process.max_concurrent,
maxRuntimeMinutes: config.process.max_runtime_minutes,
bufferSize: config.process.buffer_size,
});
for (const tool of createProcessTools(processManager)) {
toolRegistry.register(tool);
}
lifecycle.onShutdown(async () => {
await processManager.shutdown();
console.log('Process manager stopped');
});
const toolExecutor = new ToolExecutor(toolRegistry, hookEngine);
// Initialize MCP manager and start configured servers
@@ -321,6 +368,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
toolRegistry,
toolExecutor,
config,
memoryStore,
}));
// Register Telegram adapter
@@ -331,6 +379,28 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
});
channelRegistry.register(telegramAdapter);
// Register Discord adapter (if configured)
if (config.discord) {
const discordAdapter = new DiscordAdapter({
botToken: config.discord.bot_token,
allowedGuildIds: config.discord.allowed_guild_ids.length > 0 ? config.discord.allowed_guild_ids : undefined,
allowedChannelIds: config.discord.allowed_channel_ids.length > 0 ? config.discord.allowed_channel_ids : undefined,
requireMention: config.discord.require_mention,
});
channelRegistry.register(discordAdapter);
}
// Register Slack adapter (if configured)
if (config.slack) {
const slackAdapter = new SlackAdapter({
botToken: config.slack.bot_token,
appToken: config.slack.app_token,
signingSecret: config.slack.signing_secret,
allowedChannelIds: config.slack.allowed_channel_ids.length > 0 ? config.slack.allowed_channel_ids : undefined,
});
channelRegistry.register(slackAdapter);
}
// Register WebChat adapter (wraps the gateway)
const webChatAdapter = new WebChatAdapter({ gateway });
channelRegistry.register(webChatAdapter);
+28
View File
@@ -4,15 +4,29 @@ export { fileWriteTool } from './file-write.js';
export { fileEditTool } from './file-edit.js';
export { fileListTool } from './file-list.js';
export { webFetchTool } from './web-fetch.js';
export { createMemoryReadTool } from './memory-read.js';
export { createMemoryWriteTool } from './memory-write.js';
export { createMemorySearchTool } from './memory-search.js';
export { createWebSearchTool } from './web-search.js';
export type { WebSearchConfig } from './web-search.js';
export { createProcessTools, ProcessManager } from './process/index.js';
export type { ProcessManagerConfig } from './process/index.js';
import type { Tool } from '../types.js';
import type { MemoryStore } from '../../memory/store.js';
import type { WebSearchConfig } from './web-search.js';
import { shellExecTool } from './shell.js';
import { fileReadTool } from './file-read.js';
import { fileWriteTool } from './file-write.js';
import { fileEditTool } from './file-edit.js';
import { fileListTool } from './file-list.js';
import { webFetchTool } from './web-fetch.js';
import { createMemoryReadTool } from './memory-read.js';
import { createMemoryWriteTool } from './memory-write.js';
import { createMemorySearchTool } from './memory-search.js';
import { createWebSearchTool } from './web-search.js';
/** Static builtin tools that don't require runtime dependencies. */
export const allBuiltinTools: Tool[] = [
shellExecTool,
fileReadTool,
@@ -21,3 +35,17 @@ export const allBuiltinTools: Tool[] = [
fileListTool,
webFetchTool,
];
/** Create memory tools that require a MemoryStore instance. */
export function createMemoryTools(store: MemoryStore): Tool[] {
return [
createMemoryReadTool(store),
createMemoryWriteTool(store),
createMemorySearchTool(store),
];
}
/** Create the web search tool with provider config. */
export function createWebSearchTools(config: WebSearchConfig): Tool[] {
return [createWebSearchTool(config)];
}
+3 -1
View File
@@ -3,7 +3,9 @@ export { ToolRegistry } from './registry.js';
export type { AnthropicToolDef, OpenAIToolDef } from './registry.js';
export { ToolExecutor } from './executor.js';
export type { ToolExecutorConfig } from './executor.js';
export { allBuiltinTools } from './builtin/index.js';
export { allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager } from './builtin/index.js';
export type { WebSearchConfig } from './builtin/web-search.js';
export type { ProcessManagerConfig } from './builtin/process/index.js';
export { shellExecTool } from './builtin/shell.js';
export { fileReadTool } from './builtin/file-read.js';
export { fileWriteTool } from './builtin/file-write.js';